crypto-inventory 0.3.0 and the control-plane planner after the second run on a tree that is not ours (libsignal): BoringSSL, Noise hfs, libcrux Kyber, the libsignal Java/Node API; self-signed trust anchors are blocked, test code is LOW with its own scope

This commit is contained in:
Aere Network 2026-10-18 07:52:31 +03:00
parent 42b049ef04
commit e453c0c42d
35 changed files with 675 additions and 51 deletions

View File

@ -39,11 +39,11 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
| pq-gateway | 33/33 (`node proba-pq-gateway.mjs`) | 33/33 (`bash proba-pq-gateway-control-negativ.sh`) | | pq-gateway | 33/33 (`node proba-pq-gateway.mjs`) | 33/33 (`bash proba-pq-gateway-control-negativ.sh`) |
| pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) | | pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`pq-kms/test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`pq-kms/test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`pq-kms/test/control-negativ-hsm-incredere.mjs`) |
| pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) | | pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) |
| crypto-inventory | 46/46 (`node test/proba.mjs`, measured 2026-10-18 with the Rust detector, glob imports and libcrux included); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 21/21 (`node test/control-negativ.mjs`); cost 3/3 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) | | crypto-inventory | 52/52 (`node test/proba.mjs`, measured 2026-10-18 with the Rust detector, glob imports, libcrux, BoringSSL and the libsignal Java/Node API included); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 25/25 (`node test/control-negativ.mjs`); cost 3/3 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) |
| verify-layer | 45/45 with the AIP-23 reference verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools> node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) | | verify-layer | 45/45 with the AIP-23 reference verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF=<verify-proof.mjs from aere.network/tools> node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) |
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test | | proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
| readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) | | readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) |
| control-plane | planner 35/35 (2026-10-18: a key agreement or a client-side TLS configured by the application itself is not routed to the TLS gateway), command line 9/9, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 27/27 (with the AIP-23 verifier), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) | | control-plane | planner 44/44 (2026-10-18: a key agreement or a client-side TLS configured by the application itself is not routed to the TLS gateway; a self-signed trust anchor is blocked, not "automatic through PKI"; test code is LOW with its own scope), command line 10/10, execution 30/30 on real products started locally, remediation 33/33 on real TLS servers, compliance report 30/30 (with the AIP-23 verifier; test-code findings are counted apart and are not "exposed today"), console 8/8; the console viewer in a real Chromium, phone and desktop, 26/26 (`node proba-consola-web.mjs`, measured 2026-09-30; needs `playwright-core` and a Chromium, otherwise it exits 2) | remediation 7/7, compliance report 3/3 in this repository; viewer 8/8 (`node control-negativ-consola-web.mjs`) |
| identity | 46/46 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-10-05 | 53/53 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 29/29 (`node control-negativ-sdjwt.mjs`) | | identity | 46/46 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-10-05 | 53/53 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 29/29 (`node control-negativ-sdjwt.mjs`) |
| agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 54/54 on 2026-10-05, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository | | agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 54/54 on 2026-10-05, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc <solc>`) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository |
| proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) | | proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) |
@ -62,4 +62,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ...
## Licence ## Licence
MIT, see [LICENSE](LICENSE). Files: 706 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 48, verify-layer 12, proof-kinds 3, control-plane 19, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 479, device-identity 11, exposure-report 18, readiness 4). MIT, see [LICENSE](LICENSE). Files: 722 (pq-gateway 9, pq-kms 10, pq-pki 6, crypto-inventory 55, verify-layer 12, proof-kinds 3, control-plane 25, agents 39, identity 15, proof-of-ai 3, ai-trust 6, verifiable-data 7, market-settlement 9, payments 8, conformance 479, device-identity 11, exposure-report 18, readiness 4).

View File

@ -25,7 +25,15 @@ Only a finding with no call context (no `api`, no language) keeps the old routin
known, so it is not guessed. The inventory's TLS configuration findings still go to the gateway. 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, { ref, asset, problem, current, target, method: "auto-aere" | "manual" | "blocked", product: "gateway" | "kms" | "pki" | null,
how, cn?, primitive?, urgency: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW", blocker, location, source } 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 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 harvested), so a classical key exchange on a public endpoint is the only CRITICAL case, and a classical signature is a future
@ -67,7 +75,9 @@ required property is measured), `UNRESOLVED`, or `UNMEASURED` (the scan failed,
applied the change: `--applied-at`, otherwise the plan's `generatedAt`; with neither, everything is `UNMEASURED`). A domain in the 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 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`): the `node` profile end to end; for nginx, Apache and HAProxy only that OpenSSL 3.5 accepts the group list (the recipe (`measured`): the `node` profile end to end; for nginx, Apache and HAProxy only that OpenSSL 3.5 accepts the group list (the
directive comes from their documentation); Go from the 1.24 release notes, not measured. directive comes from their documentation); nginx and Go measured end to end on 2026-10-18 on real servers started from exactly the recipe's
fragments (nginx 1.31.7 with OpenSSL 3.5.8/3.5.9 in a container, Go 1.26.5): hybrid-only client accepted, TLS 1.2 refused, HSTS sent
(`dovezi/retete-2026-10-18/`); Apache and HAProxy still from their documentation.
## 4. Compliance report: `raport-conformitate.mjs` ## 4. Compliance report: `raport-conformitate.mjs`

View File

@ -44,9 +44,12 @@ function laPlanificator(g) {
// 2026-10-18: contextul apelului merge la planificator; fara el regula "in cod" (X25519 din aplicatie nu se repara cu un gateway TLS) // 2026-10-18: contextul apelului merge la planificator; fara el regula "in cod" (X25519 din aplicatie nu se repara cu un gateway TLS)
// era verde in proba functiei si moarta pe drumul real (planul Avano avea tot 65 de actiuni "auto prin gateway") // era verde in proba functiei si moarta pe drumul real (planul Avano avea tot 65 de actiuni "auto prin gateway")
api: g.api, language: g.language, protocolType: g.protocolType, api: g.api, language: g.language, protocolType: g.protocolType,
// 2026-10-18 (libsignal): codul de test se deosebeste de productie dupa cale (regula sta in plan-migrare.mjs, intr-un singur loc)
scope: scopulCaii(g.file),
location: g.file ? `${g.file}:${g.line || '?'}` : undefined, location: g.file ? `${g.file}:${g.line || '?'}` : undefined,
}; };
} }
let scopulCaii = () => 'production';
async function main() { async function main() {
const args = process.argv.slice(2); const args = process.argv.slice(2);
@ -56,7 +59,9 @@ async function main() {
if (!code) { console.error('usage: node control-plane.mjs --code <dir> [--scan scan.json] [--json]'); process.exit(2); } if (!code) { console.error('usage: node control-plane.mjs --code <dir> [--scan scan.json] [--json]'); process.exit(2); }
const inv = await import(pathToFileURL(caleaInventarului()).href); const inv = await import(pathToFileURL(caleaInventarului()).href);
const { planeaza } = await import(pathToFileURL(path.join(AICI, 'plan-migrare.mjs')).href); const plm = await import(pathToFileURL(path.join(AICI, 'plan-migrare.mjs')).href);
const { planeaza } = plm;
scopulCaii = plm.scopulCaii;
let rez; try { rez = inv.scan(code); } catch (e) { console.error('the inventory could not scan: ' + e.message); process.exit(2); } let rez; try { rez = inv.scan(code); } catch (e) { console.error('the inventory could not scan: ' + e.message); process.exit(2); }
const clasificate = (rez.findings || []).map(laPlanificator); const clasificate = (rez.findings || []).map(laPlanificator);
@ -77,7 +82,7 @@ async function main() {
if (jsonOut) { console.log(JSON.stringify(iesire, null, 1)); process.exit(0); } if (jsonOut) { console.log(JSON.stringify(iesire, null, 1)); process.exit(0); }
console.log(`AERE Control Plane - post-quantum migration plan for ${code}${scanFile ? ' + ' + scanFile : ''}`); console.log(`AERE Control Plane - post-quantum migration plan for ${code}${scanFile ? ' + ' + scanFile : ''}`);
console.log(` inventory: ${iesire.inventory.findings} uses, ${iesire.inventory.quantumVulnerable} quantum-vulnerable`); console.log(` inventory: ${iesire.inventory.findings} uses, ${iesire.inventory.quantumVulnerable} quantum-vulnerable`);
console.log(` plan: ${plan.summary.total} actions | ${JSON.stringify(plan.summary.byUrgency)} | auto=${plan.summary.autoAere} manual=${plan.summary.manual} blocked=${plan.summary.blocked}`); console.log(` plan: ${plan.summary.total} actions | ${JSON.stringify(plan.summary.byUrgency)} | auto=${plan.summary.autoAere} manual=${plan.summary.manual} blocked=${plan.summary.blocked} | in test code=${(plan.summary.byScope && plan.summary.byScope.test) || 0}`);
for (const a of plan.actions) { for (const a of plan.actions) {
console.log(` [${a.urgency.padEnd(8)}] ${a.asset}${a.location ? ' (' + a.location + ')' : ''}`); console.log(` [${a.urgency.padEnd(8)}] ${a.asset}${a.location ? ' (' + a.location + ')' : ''}`);
console.log(` ${a.problem} -> ${a.target} [${a.method}${a.product ? ' via ' + a.product : ''}]${a.blocker ? ' BLOCKED BY: ' + a.blocker : ''}`); console.log(` ${a.problem} -> ${a.target} [${a.method}${a.product ? ' via ' + a.product : ''}]${a.blocker ? ' BLOCKED BY: ' + a.blocker : ''}`);

View File

@ -0,0 +1,22 @@
# Retetele de remediere `go` si `nginx`, masurate cap la cap, 2026-10-18 04:28Z-04:31Z
Pana azi `remediere.mjs` spunea despre profilul `nginx` "the server directive from the nginx documentation, not measured by us on a
real nginx" si despre `go` "from the Go 1.24 release notes; not measured by us (the Go on the test machine is 1.22)". Masurat azi, cu
EXACT fragmentele pe care le scrie reteta pentru `hndl-exposed` + `tls12-accepted` + `hsts-missing` (setarile: grupurile
`X25519MLKEM768:X25519`, versiunea minima 1.3, HSTS un an), fiecare server pornit pe 127.0.0.1 si sondat cu Node 24 (`tls-sonda.mjs`:
o strangere de mana reala cu grupurile oferite si, optional, cu versiunea maxima) si cu `curl -k` pentru antet:
| | Go (`go/main.go`, laptop) | nginx (`masoara-nginx-rpc2.sh`, container pe rpc2) |
|---|---|---|
| serverul | Go **1.26.5** windows/amd64, `&tls.Config{MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}}`, `w.Header().Set("Strict-Transport-Security", "max-age=31536000")` | **nginx 1.31.7** din `nginx:alpine`, "built with OpenSSL 3.5.8 (running with OpenSSL 3.5.9)"; `ssl_ecdh_curve X25519MLKEM768:X25519; ssl_protocols TLSv1.3; add_header Strict-Transport-Security "max-age=31536000" always;`; `nginx -t` ok |
| client care ofera numai `X25519MLKEM768` | reusit (TLSv1.3, TLS_AES_128_GCM_SHA256) | reusit (TLSv1.3, grup raportat `X25519MLKEM768`) |
| client care ofera numai `X25519` (rezerva din reteta) | reusit, grup `X25519` | reusit, grup `X25519` |
| client `X25519MLKEM768:X25519` | reusit | reusit, grupul ales `X25519MLKEM768` |
| client TLS 1.2 | **refuzat** `ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION` | **refuzat** `ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION` |
| HSTS | `Strict-Transport-Security: max-age=31536000` | `Strict-Transport-Security: max-age=31536000` |
| scanerul AERE pe server (`scaneaza-local.mjs`, `scaneazaAdresa` pe 127.0.0.1) | `pqKeyExchange X25519MLKEM768, prefersPqWhenOffered true, tls12Accepted false, hsts true`; scor 70 numai din certificatul de proba (autosemnat, 2 zile: `cert-expiring`, `chain-untrusted`) | NEMASURAT cu scanerul (modulul nu e instalat pe rpc2 in forma importabila); sondele si curl-ul acopera aceleasi fapte |
| curatenie | serverul oprit, binarul sters | container si imagine sterse; RPC-ul local 11 -> 13 ms |
Ce NU s-a masurat: Apache si HAProxy (nimic pe nicio gazda a noastra; raman "din documentatie"), un nginx de productie cu configuratia
lui intreaga (aici un singur `server {}` de proba), Go sub Windows numai (Linux NEMASURAT; Go-ul din WSL e 1.22, sub 1.24), clienti
reali (browsere) spre aceste servere. Jurnalele: `masoara-go.log`, `masoara-nginx.log`.

View File

@ -0,0 +1,48 @@
// Serverul de proba al retetei Go din remediere.mjs (profilul `go`, pentru hndl-exposed + tls12-accepted + hsts-missing):
// &tls.Config{MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}}
// w.Header().Set("Strict-Transport-Security", "max-age=31536000")
// Certificat autosemnat generat la pornire (ECDSA P-256), asculta pe 127.0.0.1:<port>, raspunde "ok" la orice cale.
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"log"
"math/big"
"net/http"
"os"
"runtime"
"time"
)
func main() {
port := "8445"
if len(os.Args) > 1 {
port = os.Args[1]
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatal(err)
}
tmpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "proba.local"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(48 * time.Hour), DNSNames: []string{"proba.local", "localhost"}}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
log.Fatal(err)
}
cert := tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
// EXACT fragmentul retetei (remediere.mjs, profilul go, setarile groups + tlsMin 1.3 + hsts):
cfg := &tls.Config{MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}, Certificates: []tls.Certificate{cert}}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
fmt.Fprintln(w, "ok")
})
srv := &http.Server{Addr: "127.0.0.1:" + port, Handler: mux, TLSConfig: cfg}
fmt.Printf("listening 127.0.0.1:%s go=%s\n", port, runtime.Version())
log.Fatal(srv.ListenAndServeTLS("", ""))
}

View File

@ -0,0 +1,30 @@
#!/bin/bash
# masoara-go.sh: reteta Go a remedierii (profilul `go`) masurata cap la cap pe laptop: serverul din main.go (EXACT fragmentul retetei),
# construit cu Go-ul de pe Windows, pornit pe 127.0.0.1:8445; sonde Node 24 (OpenSSL 3.5.x): hibrid singur (trebuie ok), X25519 singur
# (reteta il lasa ca rezerva: trebuie ok, grup X25519), client TLS 1.2 (MinVersion 1.3: trebuie refuzat), HSTS prin curl -k, si scanerul
# AERE pe server (ce ar vedea clientul dupa reteta). Serverul e oprit la sfarsit; codul final e 0 numai cu toate cum trebuie.
set -u
D="$(cd "$(dirname "$0")" && pwd)"
cd "$D/go" || exit 2
echo "== go"; go version
go build -o proba-go-tls.exe main.go || { echo "build cazut"; exit 1; }
./proba-go-tls.exe 8445 > "$D/go-server.log" 2>&1 &
PID=$!
sleep 2
echo "server: $(head -1 "$D/go-server.log")"
rele=0
cere() { # $1 nume, $2 cod asteptat, $3... comanda
local nume="$1" ast="$2"; shift 2
out=$("$@" 2>&1); c=$?
if [ "$c" = "$ast" ]; then echo " OK $nume -> cod $c: $(printf '%s' "$out" | cut -c1-170)"; else echo " RAU $nume -> cod $c (asteptat $ast): $(printf '%s' "$out" | cut -c1-170)"; rele=$((rele + 1)); fi
}
cere "hibrid singur (X25519MLKEM768)" 0 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519MLKEM768
cere "clasic singur (X25519, rezerva din reteta)" 0 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519
cere "hibrid + clasic (preferinta)" 0 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519MLKEM768:X25519
cere "client TLS 1.2 (MinVersion 1.3: refuz)" 1 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519 TLSv1.2
echo "-- HSTS (curl -k, antetul)"; curl -sk -m 10 -D - -o /dev/null https://127.0.0.1:8445/ | grep -iE '^(strict-transport-security|HTTP)' | tr -d '\r'
echo "-- scanerul AERE pe serverul retetei"; node "$D/scaneaza-local.mjs" 8445 | cut -c1-700
kill $PID 2>/dev/null; sleep 1; ps -p $PID > /dev/null 2>&1 && { echo "serverul nu s-a oprit; taskkill"; taskkill //F //PID $PID > /dev/null 2>&1; }
rm -f proba-go-tls.exe
echo "reteta go: $((4 - rele)) din 4 sonde cum trebuie"
[ "$rele" = 0 ] && exit 0 || exit 1

View File

@ -0,0 +1,44 @@
#!/bin/bash
# masoara-nginx-rpc2.sh (PE rpc2, trimis cu `ssh ... 'bash -s'`): reteta nginx a remedierii (profilul `nginx`) masurata pe un nginx REAL,
# in container (nginx:alpine, cu OpenSSL-ul lui Alpine), pe 127.0.0.1:8446, cu EXACT directivele retetei pentru hndl-exposed +
# tls12-accepted + hsts-missing: `ssl_ecdh_curve X25519MLKEM768:X25519; ssl_protocols TLSv1.3; add_header Strict-Transport-Security
# "max-age=31536000" always;`. Preconditia retetei (nginx legat de OpenSSL >= 3.5) se citeste din `nginx -V`. Sonde: node 24 de pe rpc2
# (hibrid singur, clasic singur, TLS 1.2 refuzat), HSTS prin curl. Container si imagine sterse la sfarsit. RPC-ul local cronometrat.
set -u
D=/root/nginx-reteta; rm -rf "$D"; mkdir -p "$D/certs" "$D/conf"; cd "$D" || exit 2
N=/opt/node24/bin/node
rpc() { local t0=$(date +%s%N); curl -s -m 5 -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:8545 | grep -o '"result":"0x[0-9a-f]*"' | head -1; echo " ($(( ($(date +%s%N) - t0) / 1000000 )) ms)"; }
echo "== RPC inainte: $(rpc)"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout certs/privkey.pem -out certs/fullchain.pem -days 2 -subj '/CN=proba.local' > /dev/null 2>&1 && chmod 644 certs/*.pem && echo "cert ok"
cat > conf/default.conf <<'EOF'
server {
listen 127.0.0.1:8446 ssl;
server_name proba.local;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# EXACT fragmentul retetei (remediere.mjs, profilul nginx):
ssl_ecdh_curve X25519MLKEM768:X25519;
ssl_protocols TLSv1.3;
add_header Strict-Transport-Security "max-age=31536000" always;
location / { return 200 "ok\n"; }
}
EOF
cp tls-sonda.mjs "$D/" 2>/dev/null || true
echo "== imaginea nginx:alpine"; docker pull -q nginx:alpine > /dev/null 2>&1; docker image inspect nginx:alpine --format 'imagine: {{.Id}} {{.Size}} octeti' | cut -c1-90
echo "-- preconditia retetei: nginx -V (OpenSSL)"; docker run --rm nginx:alpine nginx -V 2>&1 | grep -iE 'nginx version|openssl' | cut -c1-160
docker rm -f nginx-reteta > /dev/null 2>&1
docker run -d --name nginx-reteta --network host -v "$D/conf/default.conf:/etc/nginx/conf.d/default.conf:ro" -v "$D/certs:/etc/nginx/certs:ro" nginx:alpine > /dev/null; sleep 2
docker ps --filter name=nginx-reteta --format 'container: {{.Status}}'
echo "-- nginx -t in container"; docker exec nginx-reteta nginx -t 2>&1 | tail -1
rele=0
cere() { local nume="$1" ast="$2"; shift 2; out=$("$@" 2>&1); c=$?; if [ "$c" = "$ast" ]; then echo " OK $nume -> cod $c: $(printf '%s' "$out" | cut -c1-170)"; else echo " RAU $nume -> cod $c (asteptat $ast): $(printf '%s' "$out" | cut -c1-170)"; rele=$((rele + 1)); fi; }
cere "hibrid singur (X25519MLKEM768)" 0 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519MLKEM768
cere "clasic singur (X25519, rezerva din reteta)" 0 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519
cere "hibrid + clasic (preferinta)" 0 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519MLKEM768:X25519
cere "client TLS 1.2 (ssl_protocols TLSv1.3: refuz)" 1 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519 TLSv1.2
echo "-- HSTS (curl -k)"; curl -sk -m 10 -D - -o /dev/null https://127.0.0.1:8446/ | grep -iE '^(strict-transport-security|HTTP)' | tr -d '\r'
docker logs nginx-reteta 2>&1 | grep -iE 'emerg|error' | head -3
echo "== curatenie"; docker rm -f nginx-reteta > /dev/null 2>&1; docker rmi nginx:alpine > /dev/null 2>&1 && echo "imaginea stearsa"; cd /root && rm -rf "$D" /root/nginx-reteta-sonda.mjs; docker ps -a --format '{{.Names}}' | grep -c nginx-reteta | sed 's/^/containere ramase: /'
echo "== RPC dupa: $(rpc)"
echo "reteta nginx: $((4 - rele)) din 4 sonde cum trebuie"
[ "$rele" = 0 ] && exit 0 || exit 1

View File

@ -0,0 +1,8 @@
// scaneaza-local.mjs <port>: scanerul AERE de pregatire post-cuantica (cloud-gateway/readiness-service.mjs, functia scaneazaAdresa,
// fara garda de adrese private: tinta e 127.0.0.1, aleasa de noi) pe serverul de proba al unei retete. Tipareste verdictul, rezumatul si
// constatarile, exact ce ar vedea un client al scanerului pe serverul lui dupa aplicarea retetei.
import { pathToFileURL } from 'node:url';
const port = Number(process.argv[2] || 8445);
const { scaneazaAdresa } = await import(pathToFileURL('C:/Users/Jessica/Documents/AereNetwork/aerenew/cloud-gateway/readiness-service.mjs').href);
const r = await scaneazaAdresa('proba.local', { address: '127.0.0.1', family: 4 }, { port, jurnal: false });
console.log(JSON.stringify({ score: r.score, verdict: r.verdict, summary: r.summary, findings: (r.findings || []).map((f) => f.id + ':' + f.severity), error: r.error, detail: r.detail }, null, 1).replace(/\n\s*/g, ' '));

View File

@ -0,0 +1,15 @@
// tls-sonda.mjs <gazda> <port> <grupuri> [maxVersion]: o strangere de mana TLS reala cu grupurile de schimb de cheie date (de ex.
// X25519MLKEM768 singur, X25519 singur) si, optional, cu versiunea maxima (TLSv1.2 = un client care nu stie TLS 1.3). Fara verificarea
// certificatului (al probei e autosemnat). JSON pe stdout; iesire 0 = handshake reusit, 1 = refuzat.
import tls from 'node:tls';
const [gazda, port, grupuri, maxV] = process.argv.slice(2);
const t0 = Date.now();
const opt = { host: gazda, port: Number(port), servername: 'proba.local', rejectUnauthorized: false, ecdhCurve: grupuri, ALPNProtocols: ['http/1.1'] };
if (maxV) { opt.maxVersion = maxV; opt.minVersion = maxV; } else { opt.minVersion = 'TLSv1.3'; opt.maxVersion = 'TLSv1.3'; }
const s = tls.connect(opt, () => {
const ek = s.getEphemeralKeyInfo();
console.log(JSON.stringify({ ok: true, grupuriOferite: grupuri, versiuneCeruta: maxV || 'TLSv1.3', protocol: s.getProtocol(), cifru: s.getCipher().name, grupRaportat: ek && ek.name, ms: Date.now() - t0, openssl: process.versions.openssl }));
s.end(); process.exitCode = 0;
});
s.setTimeout(8000, () => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, versiuneCeruta: maxV || 'TLSv1.3', motiv: 'timeout' })); s.destroy(); process.exitCode = 1; });
s.on('error', (e) => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, versiuneCeruta: maxV || 'TLSv1.3', motiv: e.code || e.message, ms: Date.now() - t0, openssl: process.versions.openssl })); process.exitCode = 1; });

View File

@ -89,6 +89,36 @@ function cnDin(subjectName) {
return m ? m[1].trim() : null; return m ? m[1].trim() : null;
} }
// 2026-10-18 (libsignal): codul de TEST (tests/, benches/, fuzz/, examples/, *Test.java, *.test.ts, ...) nu protejeaza nicio data: cheile
// lui sunt efemere si publice prin definitie. Pe libsignal 76 din 197 de actiuni (75 HIGH) erau in asemenea fisiere si ar fi stat in
// fata locurilor de productie. Constatarea ramane in plan (completitudine), dar cu urgenta LOW, scope 'test' si numarata separat.
// Scopul se deriva din CALE, aici, intr-un singur loc; adaptorul din control-plane.mjs il pune pe fiecare constatare.
const SCOP_TEST = /(^|\/)(tests?|__tests__|specs?|benches?|benchmarks?|fuzz|examples?|testdata|test-data|test-support|testing|cross-version-testing|mocks?|fixtures?)(\/|$)|(^|\/)[^/]*(_test|\.test|\.spec|Tests?|Spec|Bench|Benchmark|Fuzz)\.(rs|go|py|java|kt|ts|tsx|js|mjs|cjs|swift)$/;
export function scopulCaii(cale) {
return SCOP_TEST.test(String(cale || '').replace(/\\/g, '/')) ? 'test' : 'production';
}
const NOTA_TEST = 'test code (keys that protect no data; the production call sites come first, this one follows them or becomes the test of the hybrid path): ';
// 2026-10-18 (libsignal): un certificat AUTOSEMNAT (emitent = subiect) gasit in arbore e, de regula, o ANCORA DE INCREDERE pe care codul o
// fixeaza (radacinile AMD, Google si AWS ale atestarii in libsignal: 25, 100 si 30 de ani; sau radacina proprie a unui PKI intern). PKI-ul
// PQ nu o poate inlocui emitand un certificat: verificatorul are incredere exact in acea radacina. Si nu e WebPKI: "Amazon" in emitent nu
// face din radacina Nitro un CA public. Inainte, trei asemenea radacini ieseau "auto prin PKI" si a patra "blocata: WebPKI public", toate
// patru fals. Ce NU e ancora: un certificat de serviciu autosemnat, de scurta durata, pentru un nume de gazda (api.internal, 800 de zile,
// `openssl req -x509`): pe acela PKI-ul il inlocuieste cu unul emis pentru acelasi nume. Semnele ancorei, scrise in blocant: durata peste
// cinci ani sau un CN care nu e nume de gazda (niciun certificat de serviciu nu se emite pe decenii; radacinile numai asa se emit).
const AN_MS = 365.25 * 24 * 3600 * 1000;
function ancoraDeIncredere(f) {
const c = f.assetType === 'certificate' && f.certificate;
if (!c || !c.issuerName || !c.subjectName || String(c.issuerName).trim() !== String(c.subjectName).trim()) return null;
const cn = cnDin(c.subjectName);
const gazda = !!cn && NUME_GAZDA.test(cn.replace(/^\*\./, ''));
const ani = c.notValidBefore && c.notValidAfter ? (new Date(c.notValidAfter) - new Date(c.notValidBefore)) / AN_MS : null;
if (ani !== null && ani > 5) return { motiv: `self-signed and valid for ${Math.round(ani)} years` };
if (!gazda) return { motiv: 'self-signed and its CN is not a host name' };
return null;
}
const BLOCANT_ANCORA = 'root pinned as a trust anchor (issuer = subject): the PQ PKI cannot replace it by issuing a certificate, the verifier trusts this exact root. It changes when its owner publishes a post-quantum root: a hardware or cloud vendor root (attestation, TPM, enclaves) follows the vendor; your own root means re-rooting the internal PKI with a hybrid root. Until then keep the pin set able to carry two roots.';
function actiuneDinInventar(f) { function actiuneDinInventar(f) {
if (!vulnerabil(f)) return null; if (!vulnerabil(f)) return null;
const prim = primitivaDe(f); const prim = primitivaDe(f);
@ -105,11 +135,15 @@ function actiuneDinInventar(f) {
? `X.509 certificate ${f.certificate && f.certificate.subjectName ? '(' + f.certificate.subjectName + ')' : ''}`.trim() ? `X.509 certificate ${f.certificate && f.certificate.subjectName ? '(' + f.certificate.subjectName + ')' : ''}`.trim()
: `${f.name || prim}${f.assetType === 'related-crypto-material' && f.material ? ' (' + f.material.type + ')' : ''}`; : `${f.name || prim}${f.assetType === 'related-crypto-material' && f.material ? ' (' + f.material.type + ')' : ''}`;
// blocant onest: certificatele WebPKI publice nu se pot emite PQ azi (niciun CA public nu emite), deci manual/blocat // blocant onest: certificatele WebPKI publice nu se pot emite PQ azi (niciun CA public nu emite), deci manual/blocat
const webpkiPublic = f.assetType === 'certificate' && f.certificate && /\b(let's encrypt|digicert|globalsign|sectigo|google trust|amazon)\b/i.test((f.certificate.issuerName || '')); const ancora = ancoraDeIncredere(f);
const webpkiPublic = !ancora && f.assetType === 'certificate' && f.certificate && /\b(let's encrypt|digicert|globalsign|sectigo|google trust|amazon)\b/i.test((f.certificate.issuerName || ''));
let method = t.product ? 'auto-aere' : 'manual'; let method = t.product ? 'auto-aere' : 'manual';
let blocker = null; let cn; let blocker = null; let cn;
let how = t.how; let how = t.how;
if (f.assetType === 'certificate' && !webpkiPublic) { if (ancora) {
method = 'blocked'; blocker = `${BLOCANT_ANCORA} (${ancora.motiv})`;
how = 'replace the trust anchor, not a leaf: wait for (or ask for) the issuer\'s post-quantum root, add it next to the classical one in the pin set, then retire the classical root';
} else if (f.assetType === 'certificate' && !webpkiPublic) {
const c = cnDin(f.certificate && f.certificate.subjectName); const c = cnDin(f.certificate && f.certificate.subjectName);
if (c && NUME_GAZDA.test(c)) cn = c.toLowerCase(); if (c && NUME_GAZDA.test(c)) cn = c.toLowerCase();
else if (method === 'auto-aere') { else if (method === 'auto-aere') {
@ -122,6 +156,9 @@ function actiuneDinInventar(f) {
how = 'replace the private key together with the certificate or protocol that uses it (its use is not visible from the file)'; how = 'replace the private key together with the certificate or protocol that uses it (its use is not visible from the file)';
} }
if (webpkiPublic) { method = 'blocked'; blocker = 'public WebPKI certificate: no public CA issues post-quantum certificates yet; it stays classical until then, or the service moves behind the PQ Gateway'; } if (webpkiPublic) { method = 'blocked'; blocker = 'public WebPKI certificate: no public CA issues post-quantum certificates yet; it stays classical until then, or the service moves behind the PQ Gateway'; }
const scope = f.scope === 'test' ? 'test' : 'production';
let urgency = urgenta(prim, false, certLunga);
if (scope === 'test') { urgency = 'LOW'; how = NOTA_TEST + how; }
return { return {
ref: f.ref || `${f.assetType || 'algorithm'}:${(f.name || prim)}`, ref: f.ref || `${f.assetType || 'algorithm'}:${(f.name || prim)}`,
asset: numeAsset, asset: numeAsset,
@ -131,7 +168,8 @@ function actiuneDinInventar(f) {
method, product: method === 'auto-aere' ? t.product : null, how, method, product: method === 'auto-aere' ? t.product : null, how,
...(cn ? { cn } : {}), ...(cn ? { cn } : {}),
primitive: prim, primitive: prim,
urgency: urgenta(prim, false, certLunga), blocker, urgency, blocker,
scope,
location: f.location || null, location: f.location || null,
source: 'inventory', source: 'inventory',
}; };
@ -220,6 +258,8 @@ export function planeaza({ inventory = [], scan = null } = {}) {
manual: dedup.filter((a) => a.method === 'manual').length, manual: dedup.filter((a) => a.method === 'manual').length,
blocked: dedup.filter((a) => a.method === 'blocked').length, blocked: dedup.filter((a) => a.method === 'blocked').length,
byProduct: dedup.filter((a) => a.product).reduce((m, a) => (m[a.product] = (m[a.product] || 0) + 1, m), {}), byProduct: dedup.filter((a) => a.product).reduce((m, a) => (m[a.product] = (m[a.product] || 0) + 1, m), {}),
// 2026-10-18: actiunile din cod de test, numarate separat (scanarea nu are cod, deci 'production')
byScope: dedup.reduce((m, a) => (m[a.scope || 'production'] = (m[a.scope || 'production'] || 0) + 1, m), {}),
}; };
return { version: VERSIUNE, summary, actions: dedup }; return { version: VERSIUNE, summary, actions: dedup };
} }

View File

@ -25,6 +25,9 @@ try {
// fixtura vulnerabila // fixtura vulnerabila
const v = path.join(T, 'vuln'); fs.mkdirSync(v); const v = path.join(T, 'vuln'); fs.mkdirSync(v);
fs.writeFileSync(path.join(v, 'a.js'), "const c=require('crypto');\nc.createECDH('prime256v1');\nc.createSign('RSA-SHA256');\n"); fs.writeFileSync(path.join(v, 'a.js'), "const c=require('crypto');\nc.createECDH('prime256v1');\nc.createSign('RSA-SHA256');\n");
// 2026-10-18 (libsignal): acelasi apel intr-un fisier de TEST: in plan, dar LOW si scope 'test' (scopul se deriva din cale, prin adaptor)
fs.mkdirSync(path.join(v, 'tests'));
fs.writeFileSync(path.join(v, 'tests', 'a.test.js'), "const c=require('crypto');\nc.createECDH('prime256v1');\n");
// fixtura PQ // fixtura PQ
const pq = path.join(T, 'pq'); fs.mkdirSync(pq); const pq = path.join(T, 'pq'); fs.mkdirSync(pq);
fs.writeFileSync(path.join(pq, 'b.js'), "// ML-KEM-768 si ML-DSA-65, doar nume\nconst a='ML-KEM-768', s='ML-DSA-65';\n"); fs.writeFileSync(path.join(pq, 'b.js'), "// ML-KEM-768 si ML-DSA-65, doar nume\nconst a='ML-KEM-768', s='ML-DSA-65';\n");
@ -34,9 +37,11 @@ try {
const pv = ruleaza(v); const pv = ruleaza(v);
const byRef = Object.fromEntries(pv.actions.map((a) => [a.ref.split(':').slice(0, 3).join(':'), a])); const byRef = Object.fromEntries(pv.actions.map((a) => [a.ref.split(':').slice(0, 3).join(':'), a]));
cer('dosar vulnerabil: 2 folosiri vulnerabile', pv.inventory.quantumVulnerable === 2); cer('dosar vulnerabil: 3 folosiri vulnerabile (2 in a.js, 1 in tests/)', pv.inventory.quantumVulnerable === 3);
cer('dosar vulnerabil: 2 actiuni in plan', pv.summary.total === 2); cer('dosar vulnerabil: 3 actiuni in plan', pv.summary.total === 3);
const ecdh = pv.actions.find((a) => /ECDH/i.test(a.name || a.asset)); const inTest = pv.actions.find((a) => /tests\//.test(a.location || ''));
cer('apelul din tests/ -> scope test, urgenta LOW, numarat in byScope.test', inTest && inTest.scope === 'test' && inTest.urgency === 'LOW' && pv.summary.byScope && pv.summary.byScope.test === 1 && pv.summary.byScope.production === 2);
const ecdh = pv.actions.find((a) => /ECDH/i.test(a.name || a.asset) && a.scope !== 'test');
const rsa = pv.actions.find((a) => /RSA/i.test(a.name || a.asset)); const rsa = pv.actions.find((a) => /RSA/i.test(a.name || a.asset));
// 2026-10-18: createECDH e schimb de cheie facut de APLICATIE (api-ul si limbajul trec prin adaptor): manual, in cod, tinta hibrida, fara gateway // 2026-10-18: createECDH e schimb de cheie facut de APLICATIE (api-ul si limbajul trec prin adaptor): manual, in cod, tinta hibrida, fara gateway
cer('ECDH din cod (createECDH) -> manual, in cod, tinta X25519 + ML-KEM-768, fara gateway', ecdh && ecdh.method === 'manual' && !ecdh.product && /ML-KEM-768/.test(ecdh.target) && /createECDH/.test(ecdh.how)); cer('ECDH din cod (createECDH) -> manual, in cod, tinta X25519 + ML-KEM-768, fara gateway', ecdh && ecdh.method === 'manual' && !ecdh.product && /ML-KEM-768/.test(ecdh.target) && /createECDH/.test(ecdh.how));
@ -56,5 +61,5 @@ try {
} finally { } finally {
fs.rmSync(T, { recursive: true, force: true }); fs.rmSync(T, { recursive: true, force: true });
} }
console.log(`\nB1 control-plane CLI: ${9 - rele}/9 cum trebuia`); console.log(`\nB1 control-plane CLI: ${10 - rele}/10 cum trebuia`);
process.exit(rele ? 1 : 0); process.exit(rele ? 1 : 0);

View File

@ -24,6 +24,16 @@ const inventory = [
{ ref: 'cod:ed25519:rust', assetType: 'algorithm', name: 'Ed25519', primitive: 'signature', quantumVulnerable: true, language: 'rust', api: 'SigningKey::generate', location: 'src/sign.rs:5' }, { ref: 'cod:ed25519:rust', assetType: 'algorithm', name: 'Ed25519', primitive: 'signature', quantumVulnerable: true, language: 'rust', api: 'SigningKey::generate', location: 'src/sign.rs:5' },
{ ref: 'cod:tls:go', assetType: 'algorithm', name: 'ECDH-secp256r1', primitive: 'key-agree', quantumVulnerable: true, language: 'go', api: 'tls.Config.CurvePreferences', protocolType: undefined, location: 'main.go:30' }, { ref: 'cod:tls:go', assetType: 'algorithm', name: 'ECDH-secp256r1', primitive: 'key-agree', quantumVulnerable: true, language: 'go', api: 'tls.Config.CurvePreferences', protocolType: undefined, location: 'main.go:30' },
{ ref: 'cod:tls-client:rust', assetType: 'algorithm', name: 'X25519', primitive: 'key-agree', quantumVulnerable: true, language: 'rust', api: 'rustls::crypto::ring::default_provider', location: 'src/http.rs:12' }, { ref: 'cod:tls-client:rust', assetType: 'algorithm', name: 'X25519', primitive: 'key-agree', quantumVulnerable: true, language: 'rust', api: 'rustls::crypto::ring::default_provider', location: 'src/http.rs:12' },
// 2026-10-18 (libsignal): radacini AUTOSEMNATE fixate in arbore (ancore de incredere ale atestarii: AMD, AWS) si o constatare din cod de TEST
{ ref: 'cert:radacina-vendor', assetType: 'certificate', primitive: 'signature', quantumVulnerable: true,
certificate: { subjectName: 'OU=Engineering, O=Advanced Micro Devices, CN=ARK-Genoa', issuerName: 'OU=Engineering, O=Advanced Micro Devices, CN=ARK-Genoa', notValidBefore: '2020-10-22T00:00:00Z', notValidAfter: '2045-10-22T00:00:00Z' } },
// CN in forma de nume de gazda, dar 30 de ani: tot radacina (asa e cea reala a AWS Nitro)
{ ref: 'cert:radacina-amazon', assetType: 'certificate', primitive: 'signature', quantumVulnerable: true,
certificate: { subjectName: 'C=US, O=Amazon, OU=AWS, CN=aws.nitro-enclaves', issuerName: 'C=US, O=Amazon, OU=AWS, CN=aws.nitro-enclaves', notValidBefore: '2019-10-28T00:00:00Z', notValidAfter: '2049-10-28T00:00:00Z' } },
// CONTROL: un certificat de serviciu autosemnat, scurt, pentru un nume de gazda, NU e ancora: PKI-ul il inlocuieste pentru acelasi CN
{ ref: 'cert:autosemnat-serviciu', assetType: 'certificate', primitive: 'signature', quantumVulnerable: true,
certificate: { subjectName: 'CN=api.autosemnat.internal', issuerName: 'CN=api.autosemnat.internal', notValidBefore: '2026-01-01T00:00:00Z', notValidAfter: '2027-01-01T00:00:00Z' } },
{ ref: 'cod:x25519:test', assetType: 'algorithm', name: 'X25519', primitive: 'key-agree', quantumVulnerable: true, language: 'rust', api: 'KeyPair::generate', scope: 'test', location: 'tests/ratchet.rs:19' },
]; ];
// 2026-09-27: constatarile de scanare se DERIVA din sursa scanerului (readiness-service.mjs, apelurile lui add(id, severitate, // 2026-09-27: constatarile de scanare se DERIVA din sursa scanerului (readiness-service.mjs, apelurile lui add(id, severitate,
// titlu)), nu se scriu de mana. Fixtura veche folosea id-uri inventate ('tls-kex-classical', 'tls12-enabled'), deci proba era verde // titlu)), nu se scriu de mana. Fixtura veche folosea id-uri inventate ('tls-kex-classical', 'tls12-enabled'), deci proba era verde
@ -63,6 +73,21 @@ cer('HNDL ramane si in cod: X25519 HIGH, Ed25519 MEDIUM', xc && ec && xc.urgency
const cl = byRef['cod:tls-client:rust']; const cl = byRef['cod:tls-client:rust'];
cer('TLS de client din cod (rustls ring provider) -> manual, furnizorul hibrid in client, fara gateway', cl && cl.method === 'manual' && !cl.product && /X25519MLKEM768/.test(cl.target) && /aws-lc-rs/.test(cl.how) && /does not change what this client offers/.test(cl.how)); cer('TLS de client din cod (rustls ring provider) -> manual, furnizorul hibrid in client, fara gateway', cl && cl.method === 'manual' && !cl.product && /X25519MLKEM768/.test(cl.target) && /aws-lc-rs/.test(cl.how) && /does not change what this client offers/.test(cl.how));
// 2026-10-18 (libsignal): o radacina autosemnata e o ancora de incredere, nu un certificat pe care PKI-ul il poate emite; si nu e WebPKI
const rv = byRef['cert:radacina-vendor'], ra = byRef['cert:radacina-amazon'];
cer('radacina autosemnata a unui vendor -> BLOCATA ca ancora de incredere, fara produs, fara cn', rv && rv.method === 'blocked' && !rv.product && !rv.cn && /trust anchor/.test(rv.blocker) && /trust anchor/.test(rv.how));
cer('radacina autosemnata cu "Amazon" in emitent -> tot ancora de incredere, NU "WebPKI public"', ra && ra.method === 'blocked' && /trust anchor/.test(ra.blocker) && !/WebPKI/.test(ra.blocker));
cer('CONTROL: certificatul intern emis de alt CA (emitent != subiect) ramane auto prin PKI', byRef['cert:interna'] && byRef['cert:interna'].method === 'auto-aere');
const sa = byRef['cert:autosemnat-serviciu'];
cer('CONTROL: certificatul de serviciu autosemnat, scurt, cu nume de gazda, ramane auto prin PKI pentru acelasi CN (nu e ancora)', sa && sa.method === 'auto-aere' && sa.product === 'pki' && sa.cn === 'api.autosemnat.internal');
cer('blocantul ancorei numeste semnul (durata sau CN): vendor = durata, Amazon = durata', /valid for 25 years/.test(rv.blocker) && /valid for 30 years/.test(ra.blocker));
// 2026-10-18 (libsignal): codul de test ramane in plan, dar LOW, cu scope 'test' si numarat separat
const xt = byRef['cod:x25519:test'];
cer('X25519 din cod de test -> in plan, urgenta LOW, scope test, how spune ca e cod de test', xt && xt.urgency === 'LOW' && xt.scope === 'test' && /^test code/.test(xt.how) && /gateway in front cannot/.test(xt.how));
cer('acelasi X25519 din productie ramane HIGH si scope production', xc && xc.urgency === 'HIGH' && xc.scope === 'production');
cer('rezumatul numara scopul: byScope.test = 1', plan.summary.byScope && plan.summary.byScope.test === 1 && plan.summary.byScope.production === plan.summary.total - 1);
cer('actiunile de test stau la coada planului (sortare pe urgenta)', plan.actions[plan.actions.length - 1].scope === 'test');
// controale negative: ce NU trebuie sa fie in plan // controale negative: ce NU trebuie sa fie in plan
cer('CONTROL: ML-KEM (deja PQ) NU e in plan', !byRef['algorithm:mlkem']); cer('CONTROL: ML-KEM (deja PQ) NU e in plan', !byRef['algorithm:mlkem']);
cer('CONTROL: ML-DSA (deja PQ) NU e in plan', !byRef['algorithm:mldsa']); cer('CONTROL: ML-DSA (deja PQ) NU e in plan', !byRef['algorithm:mldsa']);

View File

@ -32,6 +32,9 @@ try {
'-days', '800', '-subj', '/CN=api.intern'], { stdio: 'ignore' }); '-days', '800', '-subj', '/CN=api.intern'], { stdio: 'ignore' });
} catch (e) { nemasurat('openssl nu a putut face certificatul de proba: ' + e.message); } } catch (e) { nemasurat('openssl nu a putut face certificatul de proba: ' + e.message); }
fs.writeFileSync(path.join(T, 'serviciu.js'), "const crypto = require('crypto');\nconst e = crypto.createECDH('prime256v1');\nconst s = crypto.sign('RSA-SHA256', Buffer.from('x'), cheie);\n"); fs.writeFileSync(path.join(T, 'serviciu.js'), "const crypto = require('crypto');\nconst e = crypto.createECDH('prime256v1');\nconst s = crypto.sign('RSA-SHA256', Buffer.from('x'), cheie);\n");
// 2026-10-18 (libsignal): acelasi schimb de cheie intr-un fisier de TEST nu e "expus azi" (cheia nu protejeaza nimic) si se numara separat
fs.mkdirSync(path.join(T, 'tests'));
fs.writeFileSync(path.join(T, 'tests', 'serviciu.test.js'), "const crypto = require('crypto');\nconst e = crypto.createECDH('prime256v1');\n");
const { caleaScanerului } = await import('./control-plane.mjs'); const { caleaScanerului } = await import('./control-plane.mjs');
const scanerSrc = fs.readFileSync(caleaScanerului(), 'utf8'); const scanerSrc = fs.readFileSync(caleaScanerului(), 'utf8');
const re = /add\('([a-z0-9-]+)',\s*'([a-z]+)',\s*[`']([^`']+)[`']/g; const reale = []; let m; const re = /add\('([a-z0-9-]+)',\s*'([a-z]+)',\s*[`']([^`']+)[`']/g; const reale = []; let m;
@ -50,6 +53,12 @@ try {
// ---- raportul // ---- raportul
const { report: raport, envelope: plic, evidenceHash } = raportConformitate({ plan, organization: 'Exemplu SRL', date: '2026-09-28' }); const { report: raport, envelope: plic, evidenceHash } = raportConformitate({ plan, organization: 'Exemplu SRL', date: '2026-09-28' });
const dupa = (f) => raport.findings.find(f); const dupa = (f) => raport.findings.find(f);
// 2026-10-18 (libsignal): schimbul de cheie din tests/ e in raport, dar NU expus azi, cu scope test; cel din productie ramane expus
const kexTest = dupa((c) => /tests\//.test(c.ref) && c.nature === 'key-exchange');
const kexProd = dupa((c) => /serviciu\.js/.test(c.ref) && c.nature === 'key-exchange');
cer('schimbul de cheie din cod de test: in raport, scope test, NU expus azi (false, nu null)', kexTest && kexTest.scope === 'test' && kexTest.exposedToday === false);
cer('schimbul de cheie din productie ramane expus azi, scope production', kexProd && kexProd.scope === 'production' && kexProd.exposedToday === true);
cer('rezumatul: inTestCode = 1, iar expusele de azi nu il numara; verdictul spune cate sunt in cod de test', raport.summary.inTestCode === 1 && raport.summary.exposedToday === raport.findings.filter((c) => c.exposedToday === true).length && !raport.findings.some((c) => c.exposedToday === true && c.scope === 'test') && /1 in test code/.test(raport.summary.verdict));
const kex = dupa((c) => c.ref.startsWith('tls-kex:')); const kex = dupa((c) => c.ref.startsWith('tls-kex:'));
cer('schimbul de cheie clasic (scanare) e schimb-de-cheie, expus AZI, cu termenele NIST 2030 si UE 2030 (risc nedeclarat = inalt)', cer('schimbul de cheie clasic (scanare) e schimb-de-cheie, expus AZI, cu termenele NIST 2030 si UE 2030 (risc nedeclarat = inalt)',
kex && kex.nature === 'key-exchange' && kex.exposedToday && kex.regimes.some((x) => x.regime === 'nist-ir-8547' && x.deadline === '2030-12-31') kex && kex.nature === 'key-exchange' && kex.exposedToday && kex.regimes.some((x) => x.regime === 'nist-ir-8547' && x.deadline === '2030-12-31')

View File

@ -116,7 +116,10 @@ export function raportConformitate({ plan, organization, risk, date, cnsa = fals
target: a.target, blocker: a.blocker || null, regimes: reg, nearestDeadline: termene[0] || null, target: a.target, blocker: a.blocker || null, regimes: reg, nearestDeadline: termene[0] || null,
// HNDL: schimbul de cheie e expus AZI (datele se recolteaza acum); semnatura nu (falsificarea vine odata cu calculatorul cuantic) // HNDL: schimbul de cheie e expus AZI (datele se recolteaza acum); semnatura nu (falsificarea vine odata cu calculatorul cuantic)
// null = nu se stie: o cheie folosita la cifrare/transport e expusa azi, una folosita la semnatura nu // null = nu se stie: o cheie folosita la cifrare/transport e expusa azi, una folosita la semnatura nu
exposedToday: nat === 'key-exchange' ? true : nat === 'key-use-unknown' ? null : false, // 2026-10-18 (libsignal): un schimb de cheie din COD DE TEST nu protejeaza nicio data, deci nu e expus azi; se numara separat
// (pe libsignal 150 din 194 de "expuse azi" erau chei de test, si verdictul le punea in fata clientului ca recoltabile)
exposedToday: nat === 'key-exchange' ? a.scope !== 'test' : nat === 'key-use-unknown' ? null : false,
scope: a.scope === 'test' ? 'test' : 'production',
}); });
} }
const cuTermen = constatari.filter((c) => c.nearestDeadline); const cuTermen = constatari.filter((c) => c.nearestDeadline);
@ -128,6 +131,7 @@ export function raportConformitate({ plan, organization, risk, date, cnsa = fals
configuration: constatari.filter((c) => c.nature === 'configuration').length, configuration: constatari.filter((c) => c.nature === 'configuration').length,
unclassified: constatari.filter((c) => c.nature === 'unclassified').length, unclassified: constatari.filter((c) => c.nature === 'unclassified').length,
exposedToday: constatari.filter((c) => c.exposedToday === true).length, exposedToday: constatari.filter((c) => c.exposedToday === true).length,
inTestCode: constatari.filter((c) => c.scope === 'test').length,
blockedByEcosystem: constatari.filter((c) => c.method === 'blocked').length, blockedByEcosystem: constatari.filter((c) => c.method === 'blocked').length,
nearestDeadline: cuTermen.map((c) => c.nearestDeadline).sort()[0] || null, nearestDeadline: cuTermen.map((c) => c.nearestDeadline).sort()[0] || null,
euInventory2026: date <= U.deadlines.inventory ? 'this report is a dated inventory (the EU requirement for 31.12.2026)' : 'the EU inventory deadline (31.12.2026) has passed', euInventory2026: date <= U.deadlines.inventory ? 'this report is a dated inventory (the EU requirement for 31.12.2026)' : 'the EU inventory deadline (31.12.2026) has passed',
@ -138,7 +142,7 @@ export function raportConformitate({ plan, organization, risk, date, cnsa = fals
? `no action classified as vulnerable, but ${rezumat.unclassified} UNCLASSIFIED actions (not judged); the report cannot say that there is no vulnerable algorithm` ? `no action classified as vulnerable, but ${rezumat.unclassified} UNCLASSIFIED actions (not judged); the report cannot say that there is no vulnerable algorithm`
: vulnerabile === 0 : vulnerabile === 0
? 'no quantum-vulnerable public-key algorithm in what was measured' ? 'no quantum-vulnerable public-key algorithm in what was measured'
: `${vulnerabile} quantum-vulnerable assets; nearest deadline ${rezumat.nearestDeadline}${rezumat.exposedToday ? `; ${rezumat.exposedToday} exposed TODAY (harvest now, decrypt later)` : ''}${rezumat.unknownUse ? `; ${rezumat.unknownUse} of unknown use (exposure today not known)` : ''}`; : `${vulnerabile} quantum-vulnerable assets; nearest deadline ${rezumat.nearestDeadline}${rezumat.exposedToday ? `; ${rezumat.exposedToday} exposed TODAY (harvest now, decrypt later)` : ''}${rezumat.unknownUse ? `; ${rezumat.unknownUse} of unknown use (exposure today not known)` : ''}${rezumat.inTestCode ? `; ${rezumat.inTestCode} in test code (not exposed today)` : ''}`;
const raport = { const raport = {
v: 2, version: VERSIUNE, organization, date, risk: risk || null, cnsa: !!cnsa, v: 2, version: VERSIUNE, organization, date, risk: risk || null, cnsa: !!cnsa,
regimes: Object.fromEntries(Object.entries(REGIMURI).filter(([k]) => cnsa || k !== 'cnsa-2.0')), regimes: Object.fromEntries(Object.entries(REGIMURI).filter(([k]) => cnsa || k !== 'cnsa-2.0')),

View File

@ -28,10 +28,10 @@ const HSTS = 'max-age=31536000';
// ce masuratoare sustine fiecare profil (un mesaj nu afirma mai mult decat masuratoarea) // ce masuratoare sustine fiecare profil (un mesaj nu afirma mai mult decat masuratoarea)
export const RETETE_MASURATE = { export const RETETE_MASURATE = {
node: 'measured end to end on 2026-09-28: a Node 24.14.1 / OpenSSL 3.5.5 server with the options of this recipe, scanned by the AERE scanner (proba-remediere.mjs)', node: 'measured end to end on 2026-09-28: a Node 24.14.1 / OpenSSL 3.5.5 server with the options of this recipe, scanned by the AERE scanner (proba-remediere.mjs)',
nginx: 'the group names accepted by OpenSSL 3.5.5 (measured); the server directive from the nginx documentation, not measured by us on a real nginx', nginx: 'measured end to end on 2026-10-18: nginx 1.31.7 (nginx:alpine, OpenSSL 3.5.8/3.5.9) with exactly these directives, in a container on 127.0.0.1: a client offering only X25519MLKEM768 completes TLS 1.3, X25519 alone still works as the fallback, a TLS 1.2 client is refused, HSTS is sent (dovezi/retete-2026-10-18/)',
apache: 'the group names accepted by OpenSSL 3.5.5 (measured); the server directive from the mod_ssl documentation, not measured by us on a real Apache', apache: 'the group names accepted by OpenSSL 3.5.5 (measured); the server directive from the mod_ssl documentation, not measured by us on a real Apache',
haproxy: 'the group names accepted by OpenSSL 3.5.5 (measured); the server directive from the HAProxy documentation, not measured by us on a real HAProxy', haproxy: 'the group names accepted by OpenSSL 3.5.5 (measured); the server directive from the HAProxy documentation, not measured by us on a real HAProxy',
go: 'from the Go 1.24 release notes (X25519MLKEM768 is the default when CurvePreferences is nil); not measured by us (the Go on the test machine is 1.22)', go: 'measured end to end on 2026-10-18: a Go 1.26.5 (windows/amd64) net/http server with exactly this tls.Config and header, on 127.0.0.1: X25519MLKEM768 alone completes TLS 1.3, X25519 alone works as the fallback, a TLS 1.2 client is refused, HSTS is sent, and the AERE scanner reports pqKeyExchange X25519MLKEM768 / tls12Accepted false / hsts true (dovezi/retete-2026-10-18/); Linux not measured (the WSL Go is 1.22)',
}; };
// --- forma structurata: ce cere fiecare defect al scanerului -------------------------------------------------------------------- // --- forma structurata: ce cere fiecare defect al scanerului --------------------------------------------------------------------

View File

@ -19,11 +19,11 @@ An API name that appears only inside a comment or inside a string literal is not
| Language | What is recognized | | Language | What is recognized |
|---|---| |---|---|
| JavaScript / TypeScript | `node:crypto` (`createHash`, `createHmac`, `createCipheriv`, `createSign`/`createVerify`, `generateKeyPair(Sync)` for `rsa`, `rsa-pss`, `dsa`, `ec`, `ed25519`, `x25519`, `dh`, `ml-dsa-*`, `ml-kem-*`, `slh-dsa-*`, `createECDH`, `getDiffieHellman`, `publicEncrypt`, `pbkdf2`, `hkdf`, `crypto.sign`/`verify`, `createPrivateKey`/`createPublicKey`, `X509Certificate`); WebCrypto `subtle` (RSA-OAEP, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, ECDH, Ed25519, X25519, AES-GCM/CBC/CTR/KW, HMAC, HKDF, PBKDF2, `digest`); TLS options `minVersion`, `maxVersion`, `secureProtocol`, `ecdhCurve`; libraries from imports: `ethers`, `web3`, `viem` (secp256k1 ECDSA), `secp256k1`, `elliptic`, `node-forge`, `jsonwebtoken`/`jose` (JWS algorithms), `@noble/curves`, `@noble/post-quantum`, `@noble/hashes`, `crypto-js`, `tweetnacl`/libsodium, `node-rsa`, bcrypt/argon2 | | JavaScript / TypeScript | `node:crypto` (`createHash`, `createHmac`, `createCipheriv`, `createSign`/`createVerify`, `generateKeyPair(Sync)` for `rsa`, `rsa-pss`, `dsa`, `ec`, `ed25519`, `x25519`, `dh`, `ml-dsa-*`, `ml-kem-*`, `slh-dsa-*`, `createECDH`, `getDiffieHellman`, `publicEncrypt`, `pbkdf2`, `hkdf`, `crypto.sign`/`verify`, `createPrivateKey`/`createPublicKey`, `X509Certificate`); WebCrypto `subtle` (RSA-OAEP, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, ECDH, Ed25519, X25519, AES-GCM/CBC/CTR/KW, HMAC, HKDF, PBKDF2, `digest`); TLS options `minVersion`, `maxVersion`, `secureProtocol`, `ecdhCurve`; libraries from imports: `ethers`, `web3`, `viem` (secp256k1 ECDSA), `secp256k1`, `elliptic`, `node-forge`, `jsonwebtoken`/`jose` (JWS algorithms), `@noble/curves`, `@noble/post-quantum`, `@noble/hashes`, `crypto-js`, `tweetnacl`/libsodium, `node-rsa`, bcrypt/argon2, `@signalapp/libsignal-client` (`PrivateKey.generate`, `IdentityKeyPair.generate`, `KEMKeyPair.generate`, `hkdf`, `Aes256GcmSiv.new`, `PreKeyBundle.new` / `processPreKeyBundle`, anchored on the package import) |
| Python | `cryptography.hazmat` (`rsa`, `ec`, `dsa`, `dh`, `ed25519`, `x25519`, `padding`, `hashes`, `algorithms`/`modes`, AEAD classes); PyCryptodome (`Crypto`/`Cryptodome`: `RSA`, `DSA`, `ECC`, ciphers and modes, `PKCS1_OAEP`, `pss`, `DSS`, hash `.new`); `hashlib` (including `hashlib.new` and `pbkdf2_hmac`); `hmac` with `digestmod`; `ecdsa`; PyJWT / python-jose algorithms; `oqs` (liboqs-python); `ssl` protocol constants and `TLSVersion` | | Python | `cryptography.hazmat` (`rsa`, `ec`, `dsa`, `dh`, `ed25519`, `x25519`, `padding`, `hashes`, `algorithms`/`modes`, AEAD classes); PyCryptodome (`Crypto`/`Cryptodome`: `RSA`, `DSA`, `ECC`, ciphers and modes, `PKCS1_OAEP`, `pss`, `DSS`, hash `.new`); `hashlib` (including `hashlib.new` and `pbkdf2_hmac`); `hmac` with `digestmod`; `ecdsa`; PyJWT / python-jose algorithms; `oqs` (liboqs-python); `ssl` protocol constants and `TLSVersion` |
| Java | `KeyPairGenerator`, `Signature`, `Cipher`, `KeyAgreement`, `MessageDigest`, `Mac`, `KeyGenerator`, `KeyFactory`, `SecretKeyFactory`, `KEM`, `SSLContext` `getInstance("...")`; `ECGenParameterSpec`, `NamedParameterSpec`; `setEnabledProtocols`; JSSE system properties (`jdk.tls.namedGroups`, protocol lists); Bouncy Castle imports and post-quantum parameter sets (`org.bouncycastle.pqc`, `MLKEMParameters`, `MLDSAParameters`, `SLHDSAParameters`, `FalconParameters`, ...) | | Java | `KeyPairGenerator`, `Signature`, `Cipher`, `KeyAgreement`, `MessageDigest`, `Mac`, `KeyGenerator`, `KeyFactory`, `SecretKeyFactory`, `KEM`, `SSLContext` `getInstance("...")`; `ECGenParameterSpec`, `NamedParameterSpec`; `setEnabledProtocols`; JSSE system properties (`jdk.tls.namedGroups`, protocol lists); Bouncy Castle imports and post-quantum parameter sets (`org.bouncycastle.pqc`, `MLKEMParameters`, `MLDSAParameters`, `SLHDSAParameters`, `FalconParameters`, ...); the libsignal Java/Android API (`org.signal.libsignal`, anchored on its import or package: `ECKeyPair.generate`, `IdentityKeyPair.generate`, `KEMKeyPair.generate(KEMKeyType.KYBER_1024)`, `HKDF.deriveSecrets`, `new Aes256GcmSiv` / `Aes256GcmEncryption` / `Aes256Ctr32`, `new PreKeyBundle`) |
| Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) | | Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) |
| Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::<Sha256>`, `Hkdf::<Sha512>`, `type HmacSha256 = Hmac<Sha256>`, `SigningKey::<Sha2_128s>`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::<Sha256>`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor<Aes128>`, `cbc::Decryptor<Aes256>`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr`; libcrux (`libcrux_ml_kem::mlkem1024::generate_key_pair`, `libcrux_ml_dsa::ml_dsa_65::sign`); a name brought in by a glob import (`use libsignal_protocol::*`) is resolved by trying the glob-imported crates in order | | Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::<Sha256>`, `Hkdf::<Sha512>`, `type HmacSha256 = Hmac<Sha256>`, `SigningKey::<Sha2_128s>`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::<Sha256>`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor<Aes128>`, `cbc::Decryptor<Aes256>`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` and the BoringSSL bindings `boring` / `boring-signal` with the same API (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `SslConnector::builder`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr`; libcrux (`libcrux_ml_kem::mlkem1024::generate_key_pair`, the pre-standard `kyber768` / `kyber1024` modules, `libcrux_ml_dsa::ml_dsa_65::sign`); `x25519_dalek::x25519(...)`; `HmacReset`; a mode type brought in by name (`use cbc::Decryptor; Decryptor::<Aes256>::new`); the hybrid Noise pattern `Noise_NKhfs_25519+Kyber1024_...` as one hybrid key exchange; a name brought in by a glob import (`use libsignal_protocol::*`) is resolved by trying the glob-imported crates in order |
| Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys | | Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys |
| Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `go.mod`, `Cargo.toml`: cryptographic libraries that are declared | | Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `go.mod`, `Cargo.toml`: cryptographic libraries that are declared |
@ -194,14 +194,22 @@ console.log(renderSummary(result));
## Tests ## Tests
``` ```
node test/proba.mjs # 37 probes: fixtures per language with the exact expected findings node test/proba.mjs # 52 probes: fixtures per language with the exact expected findings
node test/control-negativ.mjs # breaks the scanner in a copy, one rule at a time (18 mutations), and node test/control-negativ.mjs # breaks the scanner in a copy, one rule at a time (25 mutations), and
# requires the matching probe to fail while the rest of the suite still runs # requires the matching probe to fail while the rest of the suite still runs
node test/proba-timp.mjs # 8 hostile inputs, each at two sizes: the cost must grow linearly node test/proba-timp.mjs # 8 hostile inputs, each at two sizes: the cost must grow linearly
node test/control-negativ-timp.mjs # puts each quadratic form back and requires the cost probe to turn red node test/control-negativ-timp.mjs # puts each quadratic form back and requires the cost probe to turn red
node test/echivalenta-0.1.0.mjs # same findings as 0.1.0 (needs the git history; skipped without it) node test/echivalenta-0.1.0.mjs # same findings as 0.1.0 (needs the git history; skipped without it); detectors
# added after 0.1.0 (Rust, the libsignal Java/Node API) are counted apart, and
# a finding that 0.1.0 had and the current version lacks is always a difference
``` ```
Measured on 2026-10-18 on the libsignal tree (1,788 files; 596 Rust, 343 Java, 173 TypeScript, 210 Swift, 111 Kotlin): before the
additions of that day the inventory returned 450 findings, all in Rust and Python, and zero for Java and TypeScript, because
libsignal does not call JCA or `node:crypto` but its own API over the native bridge; Swift and Kotlin (20.5% of the source lines)
are not analyzed at all. After: 759 findings (Java 262). The Node side of libsignal itself still returns zero, because its own
tree imports by relative path, not by the package name the detector anchors on.
Fixtures mark each expected finding on its own line (`EXPECT: <name> | <class>`), and a probe fails on Fixtures mark each expected finding on its own line (`EXPECT: <name> | <class>`), and a probe fails on
a missing finding, an extra finding, a wrong line or a wrong class. Negative fixtures cover comments, a missing finding, an extra finding, a wrong line or a wrong class. Negative fixtures cover comments,
docstrings, strings that mention crypto APIs, a file without cryptography, algorithms from docstrings, strings that mention crypto APIs, a file without cryptography, algorithms from

View File

@ -289,7 +289,10 @@ export function evalueaza(a) {
} }
if (g === 'HYBRID-KEX') { if (g === 'HYBRID-KEX') {
const pq = /1024/.test(a.nume) ? 5 : 3; const pq = /1024/.test(a.nume) ? 5 : 3;
return f({ nume: a.nume, primitiv: 'kem', ...rez({ c: CLS.S, motiv: `${a.nume}: hybrid key exchange (classical ECDH + ML-KEM); stays secure if either component holds. The NIST category given is that of the ML-KEM component.`, q: pq }) }); const nota = a.nota ? ` ${a.nota}` : '';
// 2026-10-18 (libsignal): un hibrid cu Kyber pre-standard (Noise hfs) ramane hibrid, dar poarta recomandarea de trecere la ML-KEM
if (/kyber/i.test(a.nume)) return f({ nume: a.nume, primitiv: 'kem', ...rez({ c: CLS.S, motiv: `${a.nume}: hybrid key exchange (classical ECDH + Kyber, the pre-standard form of ML-KEM); stays secure if either component holds. The NIST category given is that of the Kyber component.${nota}`, rec: REC.PREPQ, q: pq }) });
return f({ nume: a.nume, primitiv: 'kem', ...rez({ c: CLS.S, motiv: `${a.nume}: hybrid key exchange (classical ECDH + ML-KEM); stays secure if either component holds. The NIST category given is that of the ML-KEM component.${nota}`, q: pq }) });
} }
if (g === 'TLS') return tls(a, f); if (g === 'TLS') return tls(a, f);
if (g === 'JWT-HMAC') { if (g === 'JWT-HMAC') {

View File

@ -237,9 +237,44 @@ export function detecteazaJava(ctx) {
const act = bcParam(m[1], m[2]); const act = bcParam(m[1], m[2]);
if (act) ctx.adauga(m.index, `${m[1]}Parameters.${m[2]}`, 'call', act, { bucata: m[0] }); if (act) ctx.adauga(m.index, `${m[1]}Parameters.${m[2]}`, 'call', act, { bucata: m[0] });
} }
detecteazaLibsignalJava(ctx);
void curbaCanonica; void curbaCanonica;
} }
// libsignal, API-ul Java/Android (org.signal.libsignal; 2026-10-18, dupa rularea pe arborele libsignal: 343 de fisiere Java si zero
// constatari, fiindca biblioteca nu trece prin JCA, ci prin propriile clase peste puntea nativa). Ancora e importul sau pachetul
// org.signal.libsignal, ca numele scurte (ECKeyPair, HKDF, Aes256GcmSiv) sa nu fie luate drept libsignal in alt cod.
const LIBSIGNAL_NOTA_EC = 'libsignal Curve25519 key pair: used for Diffie-Hellman (X3DH/PQXDH, Double Ratchet) and for XEdDSA signatures over the same key.';
const LIBSIGNAL_NOTA_KYBER = 'Signal PQXDH prekeys: CRYSTALS-Kyber (the pre-standard form of ML-KEM).';
const LIBSIGNAL_BUNDLE = { grup: 'LIB-MULTI', nume: 'libsignal session setup (X3DH/PQXDH)', motiv: 'libsignal prekey bundle: Curve25519 identity and signed prekey; a Kyber prekey in the bundle makes the session PQXDH (hybrid). Which prekeys the bundle carries is decided at run time.' };
export function kemLibsignal(tip) {
let k;
if ((k = /^KYBER_?(768|1024)$/i.exec(tip || ''))) return { grup: 'PREPQ', nume: `Kyber-${k[1]}`, primitiv: 'kem', functii: ['keygen'], nota: LIBSIGNAL_NOTA_KYBER };
if ((k = /^ML_?KEM_?(768|1024)$/i.exec(tip || ''))) return { grup: 'MLKEM', param: k[1], functii: ['keygen'] };
return { grup: 'UNKNOWN', nume: 'libsignal KEM key pair', motiv: 'libsignal KEMKeyPair: the key type (Kyber-1024 or ML-KEM) is the KEMKeyType argument, not visible on this line.' };
}
function detecteazaLibsignalJava(ctx) {
const ancore = ctx.potriviri(/(?<![\w.])(?:import\s+(?:static\s+)?org\.signal\.libsignal\.(?:protocol|crypto|metadata|zkgroup|svr|keytrans)\b|package\s+org\.signal\.libsignal\b)/g);
if (!ancore.length) return;
for (const m of ctx.potriviri(/(?<![\w.])(?:(ECKeyPair|IdentityKeyPair|ECPrivateKey)\s*\.\s*generate|(Curve)\s*\.\s*generateKeyPair)\s*\(/g)) {
ctx.adauga(m.index, `${m[1] || m[2]}.${m[1] ? 'generate' : 'generateKeyPair'}`, 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: LIBSIGNAL_NOTA_EC }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])KEMKeyPair\s*\.\s*generate\s*\(\s*(?:KEMKeyType\s*\.\s*([A-Za-z_0-9]+))?/g)) {
ctx.adauga(m.index, 'KEMKeyPair.generate', 'call', kemLibsignal(m[1]), { bucata: m[0].trim() });
}
for (const m of ctx.potriviri(/(?<![\w.])HKDF\s*\.\s*deriveSecrets\s*\(/g)) {
ctx.adauga(m.index, 'HKDF.deriveSecrets', 'call', { grup: 'KDF', nume: 'HKDF', hash: 'SHA256', functii: ['keyderive'], nota: 'libsignal HKDF is HKDF-SHA-256.' }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])new\s+(Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32)\s*\(/g)) {
const mod = m[1] === 'Aes256GcmSiv' ? 'gcm-siv' : m[1] === 'Aes256Ctr32' ? 'ctr' : 'gcm';
ctx.adauga(m.index, `new ${m[1]}`, 'call', { grup: 'CIPHER', nume: 'AES', param: '256', mod, functii: [m[1] === 'Aes256GcmDecryption' ? 'decrypt' : 'encrypt'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w.])new\s+PreKeyBundle\s*\(/g)) ctx.adauga(m.index, 'new PreKeyBundle', 'call', LIBSIGNAL_BUNDLE, { bucata: m[0] });
// importul: dovada slaba, o singura data pe fisier, suprimata de orice apel recunoscut de mai sus
const imp = ancore.find((a) => /^import/.test(a[0]));
if (imp) ctx.adauga(imp.index, 'import org.signal.libsignal', 'import', { grup: 'LIB-MULTI', nume: 'libsignal (org.signal.libsignal)', motiv: 'Imports the Signal protocol library (Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet, SPQR, sealed sender); no specific call recognized in this file.' }, { suprimaDe: ['XDH', 'PREPQ', 'MLKEM', 'KDF', 'CIPHER', 'LIB-MULTI'], bucata: imp[0].trim() });
}
function bcParam(fam, p) { function bcParam(fam, p) {
let m; let m;
if (fam === 'MLKEM' && (m = /(512|768|1024)/.exec(p))) return { grup: 'MLKEM', param: m[1] }; if (fam === 'MLKEM' && (m = /(512|768|1024)/.exec(p))) return { grup: 'MLKEM', param: m[1] };

View File

@ -1,5 +1,5 @@
// Detectorul JavaScript / TypeScript: node:crypto, WebCrypto, optiuni TLS, biblioteci din importuri. // Detectorul JavaScript / TypeScript: node:crypto, WebCrypto, optiuni TLS, biblioteci din importuri.
import { argumente, imparteArgumente, valoareArgument, necunoscut, obiectulDin, cifruOpenssl, identificator, rezolva } from './context.mjs'; import { argumente, imparteArgumente, valoareArgument, necunoscut, obiectulDin, cifruOpenssl, identificator, rezolva, escapeRe } from './context.mjs';
import { hashCanonic, grupTls, jws, JWS_RE } from './catalog.mjs'; import { hashCanonic, grupTls, jws, JWS_RE } from './catalog.mjs';
const MODP = { modp1: 768, modp2: 1024, modp5: 1536, modp14: 2048, modp15: 3072, modp16: 4096, modp17: 6144, modp18: 8192 }; const MODP = { modp1: 768, modp2: 1024, modp5: 1536, modp14: 2048, modp15: 3072, modp16: 4096, modp17: 6144, modp18: 8192 };
@ -487,6 +487,38 @@ function detecteazaBiblioteci(ctx, imp, importa, jwtLib) {
ctx.adauga(nacl[0].pos, `import ${nacl[0].spec}`, 'import', { grup: 'LIB-MULTI', nume: nacl[0].spec }, { suprimaDe: ['EDDSA', 'XDH', 'CIPHER'], bucata: `import ${nacl[0].spec}` }); ctx.adauga(nacl[0].pos, `import ${nacl[0].spec}`, 'import', { grup: 'LIB-MULTI', nume: nacl[0].spec }, { suprimaDe: ['EDDSA', 'XDH', 'CIPHER'], bucata: `import ${nacl[0].spec}` });
} }
// @signalapp/libsignal-client (Node; 2026-10-18, dupa rularea pe arborele libsignal: 173 de fisiere TypeScript si zero constatari,
// fiindca API-ul lui sta peste puntea nativa, nu peste node:crypto). Ancora e importul pachetului: numele scurte (PrivateKey, hkdf)
// nu inseamna libsignal in alt cod. Arborele libsignal insusi importa prin cai relative, deci acolo ramane nevazut; aici se recunoaste
// ce foloseste un CLIENT al bibliotecii.
const lsc = importa(/^@signalapp\/libsignal-client(\/.*)?$/);
if (lsc.length) {
const notaEc = 'libsignal Curve25519 key pair: used for Diffie-Hellman (X3DH/PQXDH, Double Ratchet) and for XEdDSA signatures over the same key.';
const bundle = { grup: 'LIB-MULTI', nume: 'libsignal session setup (X3DH/PQXDH)', motiv: 'libsignal prekey bundle: Curve25519 identity and signed prekey; a Kyber prekey in the bundle makes the session PQXDH (hybrid). Which prekeys the bundle carries is decided at run time.' };
for (const m of ctx.potriviri(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?(PrivateKey|IdentityKeyPair)\s*\.\s*generate\s*\(/g)) {
ctx.adauga(m.index, `${m[1]}.generate`, 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: notaEc }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?KEMKeyPair\s*\.\s*generate\s*\(/g)) {
ctx.adauga(m.index, 'KEMKeyPair.generate', 'call', { grup: 'PREPQ', nume: 'Kyber-1024', primitiv: 'kem', functii: ['keygen'], nota: 'libsignal-client KEMKeyPair.generate() produces a Kyber-1024 pair (the PQXDH prekey; the only KEM type the Node API exposed when this rule was written).' }, { bucata: m[0] });
}
// hkdf: numai numele importat din pachet sau prin aliasul lui de spatiu de nume (node:crypto are si el un hkdf, recunoscut mai sus)
const numeHkdf = lsc.flatMap((i) => i.locale.filter((_, k) => i.importate[k] === 'hkdf'));
const aliasuri = lsc.map((i) => i.implicit).filter(Boolean);
const forme = [...numeHkdf.map((n) => `${escapeRe(n)}`), ...aliasuri.map((a) => `${escapeRe(a)}\\s*\\.\\s*hkdf`)];
if (forme.length) {
for (const m of ctx.potriviri(new RegExp(`(?<![\\w$.])(?:${forme.join('|')})\\s*\\(`, 'g'))) {
ctx.adauga(m.index, 'hkdf', 'call', { grup: 'KDF', nume: 'HKDF', hash: 'SHA256', functii: ['keyderive'], nota: 'libsignal HKDF is HKDF-SHA-256.' }, { bucata: m[0] });
}
}
for (const m of ctx.potriviri(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?Aes256GcmSiv\s*\.\s*new\s*\(/g)) {
ctx.adauga(m.index, 'Aes256GcmSiv.new', 'call', { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'gcm-siv', functii: ['encrypt'] }, { bucata: m[0] });
}
for (const m of ctx.potriviri(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?(PreKeyBundle\s*\.\s*new|processPreKeyBundle)\s*\(/g)) {
ctx.adauga(m.index, m[1].replace(/\s+/g, ''), 'call', bundle, { bucata: m[0] });
}
ctx.adauga(lsc[0].pos, `import ${lsc[0].spec}`, 'import', { grup: 'LIB-MULTI', nume: '@signalapp/libsignal-client', motiv: 'Imports the Signal protocol library for Node (Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet, SPQR, sealed sender); no specific call recognized in this file.' }, { suprimaDe: ['XDH', 'PREPQ', 'MLKEM', 'KDF', 'CIPHER', 'LIB-MULTI'], bucata: `import ${lsc[0].spec}` });
}
// node-rsa // node-rsa
const nrsa = importa(/^node-rsa$/); const nrsa = importa(/^node-rsa$/);
if (nrsa.length) { if (nrsa.length) {

View File

@ -68,6 +68,11 @@ const CRATE = {
libsignal_protocol: { ofera: 'Signal sessions: Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet', grupuri: ['XDH', 'PREPQ', 'MLKEM', 'LIB-MULTI'] }, libsignal_protocol: { ofera: 'Signal sessions: Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet', grupuri: ['XDH', 'PREPQ', 'MLKEM', 'LIB-MULTI'] },
ring: { ofera: 'ring: AES-GCM, ChaCha20-Poly1305, SHA-2, HMAC, HKDF, PBKDF2, Ed25519, ECDSA P-256/P-384, RSA, X25519, ECDH', grupuri: ['HASH', 'MAC', 'KDF', 'CIPHER', 'EDDSA', 'ECDSA', 'RSA', 'XDH', 'ECDH'] }, ring: { ofera: 'ring: AES-GCM, ChaCha20-Poly1305, SHA-2, HMAC, HKDF, PBKDF2, Ed25519, ECDSA P-256/P-384, RSA, X25519, ECDH', grupuri: ['HASH', 'MAC', 'KDF', 'CIPHER', 'EDDSA', 'ECDSA', 'RSA', 'XDH', 'ECDH'] },
openssl: { ofera: 'OpenSSL bindings', grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] }, openssl: { ofera: 'OpenSSL bindings', grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] },
// 2026-10-18 (libsignal): BoringSSL bindings, acelasi API ca `openssl`; `boring-signal` e forkul lui Signal, stiva TLS a lui libsignal
boring: { ofera: 'BoringSSL bindings (same API as openssl)', grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] },
boring_signal: { ofera: "BoringSSL bindings (Signal's fork of boring; the TLS stack under libsignal)", grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] },
tokio_boring: { ofera: 'TLS over tokio (BoringSSL)', grupuri: ['TLS'] },
tokio_boring_signal: { ofera: 'TLS over tokio (BoringSSL, Signal fork)', grupuri: ['TLS'] },
rustls: { ofera: 'TLS (versions and key-exchange groups come from the crypto provider and the Cargo features)', grupuri: ['TLS', 'XDH', 'UNKNOWN'] }, rustls: { ofera: 'TLS (versions and key-exchange groups come from the crypto provider and the Cargo features)', grupuri: ['TLS', 'XDH', 'UNKNOWN'] },
tokio_rustls: { ofera: 'TLS over tokio (rustls)', grupuri: ['TLS'] }, tokio_rustls: { ofera: 'TLS over tokio (rustls)', grupuri: ['TLS'] },
hyper_rustls: { ofera: 'HTTPS for hyper (rustls)', grupuri: ['TLS'] }, hyper_rustls: { ofera: 'HTTPS for hyper (rustls)', grupuri: ['TLS'] },
@ -87,7 +92,7 @@ for (const [crate, tipuri] of Object.entries({
sha1: ['Sha1'], md5: ['Md5'], sha1: ['Sha1'], md5: ['Md5'],
sha3: ['Sha3_224', 'Sha3_256', 'Sha3_384', 'Sha3_512', 'Keccak256', 'Shake128', 'Shake256'], sha3: ['Sha3_224', 'Sha3_256', 'Sha3_384', 'Sha3_512', 'Keccak256', 'Shake128', 'Shake256'],
blake2: ['Blake2b512', 'Blake2s256', 'Blake2b', 'Blake2s', 'Blake2bMac', 'Blake2sMac'], blake2: ['Blake2b512', 'Blake2s256', 'Blake2b', 'Blake2s', 'Blake2bMac', 'Blake2sMac'],
hmac: ['Hmac', 'SimpleHmac'], hkdf: ['Hkdf'], argon2: ['Argon2'], hmac: ['Hmac', 'SimpleHmac', 'HmacReset', 'SimpleHmacReset'], hkdf: ['Hkdf'], argon2: ['Argon2'],
chacha20poly1305: ['ChaCha20Poly1305', 'XChaCha20Poly1305', 'ChaCha8Poly1305', 'ChaCha12Poly1305', 'XChaCha8Poly1305', 'XChaCha12Poly1305'], chacha20poly1305: ['ChaCha20Poly1305', 'XChaCha20Poly1305', 'ChaCha8Poly1305', 'ChaCha12Poly1305', 'XChaCha8Poly1305', 'XChaCha12Poly1305'],
aes_gcm: ['Aes128Gcm', 'Aes256Gcm'], aes_gcm_siv: ['Aes128GcmSiv', 'Aes256GcmSiv'], aes_siv: ['Aes128SivAead', 'Aes256SivAead'], aes_gcm: ['Aes128Gcm', 'Aes256Gcm'], aes_gcm_siv: ['Aes128GcmSiv', 'Aes256GcmSiv'], aes_siv: ['Aes128SivAead', 'Aes256SivAead'],
aes: ['Aes128', 'Aes192', 'Aes256'], aes: ['Aes128', 'Aes192', 'Aes256'],
@ -233,7 +238,7 @@ export function detecteazaRust(ctx) {
} }
// 2. aliasuri de tip cu parametru generic: type HmacSha256 = Hmac<Sha256>; type Kdf = Hkdf<Sha512>; // 2. aliasuri de tip cu parametru generic: type HmacSha256 = Hmac<Sha256>; type Kdf = Hkdf<Sha512>;
for (const m of ctx.potriviri(/(?<![\w:])type\s+\w+\s*=\s*((?:[A-Za-z_]\w*::)*)(Hmac|SimpleHmac|Hkdf)\s*<\s*([^>;]+)>/g)) { for (const m of ctx.potriviri(/(?<![\w:])type\s+\w+\s*=\s*((?:[A-Za-z_]\w*::)*)(Hmac|SimpleHmac|HmacReset|SimpleHmacReset|Hkdf)\s*<\s*([^>;]+)>/g)) {
const r = rezolvaCale(m[1], m[2]); const r = rezolvaCale(m[1], m[2]);
if (!r) continue; if (!r) continue;
const h = hashDinTip(m[3]); const h = hashDinTip(m[3]);
@ -255,9 +260,11 @@ export function detecteazaRust(ctx) {
} }
} }
// functii importate direct: use pbkdf2::pbkdf2_hmac; use blake3::hash; use libsignal_protocol::process_prekey_bundle; // functii importate direct: use pbkdf2::pbkdf2_hmac; use blake3::hash; use libsignal_protocol::process_prekey_bundle;
// (2026-10-18: nu si DEFINITIA unei functii cu acelasi nume, `fn decapsulate(`: pe libsignal, `use libcrux_ml_kem::kyber1024::decapsulate`
// dintr-un modul de test facea din cele sase metode `fn encapsulate/decapsulate` ale trait-ului Parameters sase apeluri Kyber)
for (const [local, u] of locale) { for (const [local, u] of locale) {
if (!/^[a-z_]\w*$/.test(local) || !CRATE[u.crate] || u.cale.length < 2) continue; if (!/^[a-z_]\w*$/.test(local) || !CRATE[u.crate] || u.cale.length < 2) continue;
for (const m of ctx.potriviri(new RegExp(`(?<![\\w:.])${escapeRe(local)}\\s*(?:::\\s*<([^;{}()=]*?)>)?\\s*\\(`, 'g'))) { for (const m of ctx.potriviri(new RegExp(`(?<![\\w:.])(?<!\\bfn\\s+)${escapeRe(local)}\\s*(?:::\\s*<([^;{}()=]*?)>)?\\s*\\(`, 'g'))) {
const activ = activFunctie(u.cale, m[1], ctx, m); const activ = activFunctie(u.cale, m[1], ctx, m);
if (activ) adauga(u.crate, m.index, u.cale.join('::'), 'call', activ, m[0].trim() + (activ.bucataExtra || '')); if (activ) adauga(u.crate, m.index, u.cale.join('::'), 'call', activ, m[0].trim() + (activ.bucataExtra || ''));
} }
@ -306,17 +313,34 @@ export function detecteazaRust(ctx) {
// 5. modurile de operare ale RustCrypto: cbc::Encryptor<Aes256>, ecb::Decryptor<aes::Aes128>, ctr::Ctr128BE<Aes256> // 5. modurile de operare ale RustCrypto: cbc::Encryptor<Aes256>, ecb::Decryptor<aes::Aes128>, ctr::Ctr128BE<Aes256>
for (const m of ctx.potriviri(/(?<![\w:])(cbc|ecb|ctr|cfb_mode|ofb|ccm)::(Encryptor|Decryptor|Ctr\w*|Ccm)\s*(?:::)?\s*<\s*(?:aes::)?Aes(128|192|256)\b/g)) { for (const m of ctx.potriviri(/(?<![\w:])(cbc|ecb|ctr|cfb_mode|ofb|ccm)::(Encryptor|Decryptor|Ctr\w*|Ccm)\s*(?:::)?\s*<\s*(?:aes::)?Aes(128|192|256)\b/g)) {
const mod = m[1] === 'cfb_mode' ? 'cfb' : m[1]; const mod = m[1] === 'cfb_mode' ? 'cfb' : m[1];
folosite.add(m[1]); // crate-ul modului e folosit, nu doar aes (altfel `use cbc` iesea 'import fara apel' langa propriul lui apel)
adauga('aes', m.index, `${m[1]}::${m[2]}<Aes${m[3]}>`, 'call', { grup: 'CIPHER', nume: 'AES', param: m[3], mod, functii: [m[2] === 'Decryptor' ? 'decrypt' : 'encrypt'] }, m[0]); adauga('aes', m.index, `${m[1]}::${m[2]}<Aes${m[3]}>`, 'call', { grup: 'CIPHER', nume: 'AES', param: m[3], mod, functii: [m[2] === 'Decryptor' ? 'decrypt' : 'encrypt'] }, m[0]);
} }
// 2026-10-18 (libsignal): acelasi tip de mod adus prin `use cbc::Decryptor;` si scris fara prefix (Decryptor::<Aes256>::new)
for (const m of ctx.potriviri(/(?<![\w:])(Encryptor|Decryptor|Ctr\w*|Ccm)\s*(?:::)?\s*<\s*(?:aes::)?Aes(128|192|256)\b/g)) {
const u = locale.get(m[1]);
if (!u || !/^(cbc|ecb|ctr|cfb_mode|ofb|ccm)$/.test(u.crate)) continue;
const mod = u.crate === 'cfb_mode' ? 'cfb' : u.crate;
folosite.add(u.crate);
adauga('aes', m.index, `${u.crate}::${m[1]}<Aes${m[2]}>`, 'call', { grup: 'CIPHER', nume: 'AES', param: m[2], mod, functii: [m[1] === 'Decryptor' ? 'decrypt' : 'encrypt'] }, m[0]);
}
// 6. tiparul Noise al lui snow, scris ca sir: Noise_XX_25519_ChaChaPoly_SHA256 // 6. tiparul Noise al lui snow, scris ca sir: Noise_XX_25519_ChaChaPoly_SHA256
// 2026-10-18 (libsignal): si forma hibrida hfs, Noise_NKhfs_25519+Kyber1024_ChaChaPoly_SHA256: X25519 si KEM-ul in aceeasi strangere
// de mana (hybrid forward secrecy), deci un schimb de cheie HIBRID, nu un X25519 singur peste care sa se ceara ML-KEM
if ([...locale.values()].some((u) => u.crate === 'snow') || /(?<![\w:])snow::/.test(ctx.code)) { if ([...locale.values()].some((u) => u.crate === 'snow') || /(?<![\w:])snow::/.test(ctx.code)) {
for (const m of ctx.code.matchAll(/Noise_(\w+?)_(25519|448)_(ChaChaPoly|AESGCM)_(SHA256|SHA512|BLAKE2s|BLAKE2b)\b/g)) { for (const m of ctx.code.matchAll(/Noise_(\w+?)_(25519|448)(?:\+(Kyber|MLKEM|ML_KEM|mlkem)(512|768|1024))?_(ChaChaPoly|AESGCM)_(SHA256|SHA512|BLAKE2s|BLAKE2b)\b/g)) {
if (!ctx.inSir(m.index)) continue; if (!ctx.inSir(m.index)) continue;
const pos = m.index; const pos = m.index;
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'XDH', nume: m[2] === '448' ? 'X448' : 'X25519', functii: ['keygen'] }, m[0]); const dh = m[2] === '448' ? 'X448' : 'X25519';
adauga('snow', pos, 'snow Noise pattern', 'config', m[3] === 'AESGCM' ? { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'gcm', functii: ['encrypt'] } : { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'] }, m[0]); if (m[3]) {
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'HASH', hash: m[4] === 'SHA256' ? 'SHA256' : m[4] === 'SHA512' ? 'SHA512' : m[4] === 'BLAKE2s' ? 'BLAKE2S' : 'BLAKE2B', functii: ['digest'] }, m[0]); const kem = /kyber/i.test(m[3]) ? `Kyber${m[4]}` : `ML-KEM-${m[4]}`;
adauga('snow', pos, 'snow Noise pattern (hfs)', 'config', { grup: 'HYBRID-KEX', nume: `${dh}+${kem} (Noise hfs)`, nota: `Noise hybrid forward secrecy: the ${dh} and the ${kem} shared secrets both enter the handshake key schedule.` }, m[0]);
} else {
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'XDH', nume: dh, functii: ['keygen'] }, m[0]);
}
adauga('snow', pos, 'snow Noise pattern', 'config', m[5] === 'AESGCM' ? { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'gcm', functii: ['encrypt'] } : { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'] }, m[0]);
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'HASH', hash: m[6] === 'SHA256' ? 'SHA256' : m[6] === 'SHA512' ? 'SHA512' : m[6] === 'BLAKE2s' ? 'BLAKE2S' : 'BLAKE2B', functii: ['digest'] }, m[0]);
} }
} }
@ -328,11 +352,13 @@ export function detecteazaRust(ctx) {
} }
} }
// 8. openssl: versiunea minima/maxima TLS // 8. openssl (si boring / boring-signal, acelasi API): versiunea minima/maxima TLS
if ([...locale.values()].some((u) => u.crate === 'openssl') || /(?<![\w:])openssl::/.test(ctx.code)) { const crateSsl = [...locale.values()].map((u) => u.crate).find((c) => /^(openssl|boring|boring_signal)$/.test(c))
|| (/(?<![\w:])(openssl|boring_signal|boring)::/.exec(ctx.code) || [])[1] || null;
if (crateSsl) {
const vers = { SSL3: 'ssl3', TLS1: '1.0', TLS1_1: '1.1', TLS1_2: '1.2', TLS1_3: '1.3' }; const vers = { SSL3: 'ssl3', TLS1: '1.0', TLS1_1: '1.1', TLS1_2: '1.2', TLS1_3: '1.3' };
for (const m of ctx.potriviri(/\.\s*set_(min|max)_proto_version\s*\(\s*Some\s*\(\s*(?:openssl::ssl::)?SslVersion::(SSL3|TLS1_3|TLS1_2|TLS1_1|TLS1)\b/g)) { for (const m of ctx.potriviri(/\.\s*set_(min|max)_proto_version\s*\(\s*Some\s*\(\s*(?:(?:openssl|boring|boring_signal)::ssl::)?SslVersion::(SSL3|TLS1_3|TLS1_2|TLS1_1|TLS1)\b/g)) {
adauga('openssl', m.index, `openssl set_${m[1]}_proto_version`, 'config', { grup: 'TLS', param: vers[m[2]], rol: m[1] }, m[0]); adauga(crateSsl, m.index, `${NUME_CARGO(crateSsl)} set_${m[1]}_proto_version`, 'config', { grup: 'TLS', param: vers[m[2]], rol: m[1] }, m[0]);
} }
} }
@ -377,7 +403,7 @@ function activStatic(r, metoda, generic, ctx, m) {
case 'blake3': case 'blake3':
return tip === 'Hasher' && /^(new|new_keyed|new_derive_key|default)$/.test(metoda) ? { grup: 'HASH', hash: 'BLAKE3', functii: ['digest'] } : null; return tip === 'Hasher' && /^(new|new_keyed|new_derive_key|default)$/.test(metoda) ? { grup: 'HASH', hash: 'BLAKE3', functii: ['digest'] } : null;
case 'hmac': { case 'hmac': {
if (!/^(Hmac|SimpleHmac)$/.test(tip) || !/^(new|new_from_slice|new_varkey|default)$/.test(metoda)) return null; if (!/^(Hmac|SimpleHmac|HmacReset|SimpleHmacReset)$/.test(tip) || !/^(new|new_from_slice|new_varkey|default)$/.test(metoda)) return null;
const h = hashDinTip(generic); const h = hashDinTip(generic);
return h ? { grup: 'MAC', hash: h, functii: ['tag'], crateHash: crateHashDinTip(generic) } : { grup: 'UNKNOWN', motiv: generic ? `HMAC over "${generic.trim()}", a hash this tool does not resolve.` : 'HMAC whose hash is a type alias or an inferred type parameter, not visible at this call.' }; return h ? { grup: 'MAC', hash: h, functii: ['tag'], crateHash: crateHashDinTip(generic) } : { grup: 'UNKNOWN', motiv: generic ? `HMAC over "${generic.trim()}", a hash this tool does not resolve.` : 'HMAC whose hash is a type alias or an inferred type parameter, not visible at this call.' };
} }
@ -474,13 +500,13 @@ function activStatic(r, metoda, generic, ctx, m) {
if (tip === 'EcdsaKeyPair') { const c = /P(256|384)/.exec(ctx.code.slice(m.index, m.index + 200)); return { grup: 'ECDSA', curba: c ? `secp${c[1]}r1` : undefined, functii: fn(/generate/.test(metoda) ? 'keygen' : 'other') }; } if (tip === 'EcdsaKeyPair') { const c = /P(256|384)/.exec(ctx.code.slice(m.index, m.index + 200)); return { grup: 'ECDSA', curba: c ? `secp${c[1]}r1` : undefined, functii: fn(/generate/.test(metoda) ? 'keygen' : 'other') }; }
if (tip === 'EphemeralPrivateKey' && metoda === 'generate') { const c = /(X25519|ECDH_P256|ECDH_P384)/.exec(ctx.code.slice(m.index, m.index + 200)); return c && c[1] !== 'X25519' ? { grup: 'ECDH', curba: c[1] === 'ECDH_P256' ? 'secp256r1' : 'secp384r1', functii: ['keygen'] } : { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }; } if (tip === 'EphemeralPrivateKey' && metoda === 'generate') { const c = /(X25519|ECDH_P256|ECDH_P384)/.exec(ctx.code.slice(m.index, m.index + 200)); return c && c[1] !== 'X25519' ? { grup: 'ECDH', curba: c[1] === 'ECDH_P256' ? 'secp256r1' : 'secp384r1', functii: ['keygen'] } : { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }; }
return null; return null;
case 'openssl': { case 'openssl': case 'boring': case 'boring_signal': {
if (tip === 'MessageDigest') { const h = { md5: 'MD5', sha1: 'SHA1', sha224: 'SHA224', sha256: 'SHA256', sha384: 'SHA384', sha512: 'SHA512', sha3_224: 'SHA3-224', sha3_256: 'SHA3-256', sha3_384: 'SHA3-384', sha3_512: 'SHA3-512', ripemd160: 'RIPEMD160', shake_128: 'SHAKE128', shake_256: 'SHAKE256' }[metoda]; return h ? { grup: 'HASH', hash: h, functii: ['digest'] } : null; } if (tip === 'MessageDigest') { const h = { md5: 'MD5', sha1: 'SHA1', sha224: 'SHA224', sha256: 'SHA256', sha384: 'SHA384', sha512: 'SHA512', sha3_224: 'SHA3-224', sha3_256: 'SHA3-256', sha3_384: 'SHA3-384', sha3_512: 'SHA3-512', ripemd160: 'RIPEMD160', shake_128: 'SHAKE128', shake_256: 'SHAKE256' }[metoda]; return h ? { grup: 'HASH', hash: h, functii: ['digest'] } : null; }
if (tip === 'Cipher') { const k = /^(aes|des_ede3|des|rc4|rc2|bf|chacha20_poly1305|chacha20|camellia|sm4|aria)_?(\d{3})?_?(\w+)?$/.exec(metoda); if (!k) return null; const nume = k[1] === 'des_ede3' ? '3DES' : k[1] === 'bf' ? 'Blowfish' : k[1] === 'chacha20_poly1305' ? 'ChaCha20-Poly1305' : k[1] === 'chacha20' ? 'ChaCha20' : k[1].toUpperCase(); return { grup: 'CIPHER', nume, param: k[2], mod: k[3] ? k[3].replace(/_/g, '-') : undefined, functii: ['encrypt'] }; } if (tip === 'Cipher') { const k = /^(aes|des_ede3|des|rc4|rc2|bf|chacha20_poly1305|chacha20|camellia|sm4|aria)_?(\d{3})?_?(\w+)?$/.exec(metoda); if (!k) return null; const nume = k[1] === 'des_ede3' ? '3DES' : k[1] === 'bf' ? 'Blowfish' : k[1] === 'chacha20_poly1305' ? 'ChaCha20-Poly1305' : k[1] === 'chacha20' ? 'ChaCha20' : k[1].toUpperCase(); return { grup: 'CIPHER', nume, param: k[2], mod: k[3] ? k[3].replace(/_/g, '-') : undefined, functii: ['encrypt'] }; }
if (tip === 'Rsa' && /^(generate|generate_with_e)$/.test(metoda)) { const b = /^\s*(\d{3,5})\s*(?:,|$)/.exec(argsDupa(ctx, m)); return { grup: 'RSA', param: b ? b[1] : undefined, functii: ['keygen'], bucataExtra: b ? ` ${b[1]}` : '' }; } if (tip === 'Rsa' && /^(generate|generate_with_e)$/.test(metoda)) { const b = /^\s*(\d{3,5})\s*(?:,|$)/.exec(argsDupa(ctx, m)); return { grup: 'RSA', param: b ? b[1] : undefined, functii: ['keygen'], bucataExtra: b ? ` ${b[1]}` : '' }; }
if (tip === 'EcKey' && /^(generate|from_curve_name)$/.test(metoda)) { const c = /Nid::(X9_62_PRIME256V1|SECP256K1|SECP384R1|SECP521R1|SECP224R1|BRAINPOOL_P256R1|BRAINPOOL_P384R1|BRAINPOOL_P512R1)/.exec(ctx.code.slice(m.index, m.index + 300)); const curba = c ? ({ X9_62_PRIME256V1: 'secp256r1', SECP256K1: 'secp256k1', SECP384R1: 'secp384r1', SECP521R1: 'secp521r1', SECP224R1: 'secp224r1', BRAINPOOL_P256R1: 'brainpoolP256r1', BRAINPOOL_P384R1: 'brainpoolP384r1', BRAINPOOL_P512R1: 'brainpoolP512r1' })[c[1]] : undefined; return { grup: 'EC', curba, functii: ['keygen'] }; } if (tip === 'EcKey' && /^(generate|from_curve_name)$/.test(metoda)) { const c = /Nid::(X9_62_PRIME256V1|SECP256K1|SECP384R1|SECP521R1|SECP224R1|BRAINPOOL_P256R1|BRAINPOOL_P384R1|BRAINPOOL_P512R1)/.exec(ctx.code.slice(m.index, m.index + 300)); const curba = c ? ({ X9_62_PRIME256V1: 'secp256r1', SECP256K1: 'secp256k1', SECP384R1: 'secp384r1', SECP521R1: 'secp521r1', SECP224R1: 'secp224r1', BRAINPOOL_P256R1: 'brainpoolP256r1', BRAINPOOL_P384R1: 'brainpoolP384r1', BRAINPOOL_P512R1: 'brainpoolP512r1' })[c[1]] : undefined; return { grup: 'EC', curba, functii: ['keygen'] }; }
if (tip === 'PKey') { const k = /^generate_(ed25519|ed448|x25519|x448)$/.exec(metoda); if (!k) return null; return /^ed/.test(k[1]) ? { grup: 'EDDSA', nume: k[1] === 'ed448' ? 'Ed448' : 'Ed25519', functii: ['keygen'] } : { grup: 'XDH', nume: k[1] === 'x448' ? 'X448' : 'X25519', functii: ['keygen'] }; } if (tip === 'PKey') { const k = /^generate_(ed25519|ed448|x25519|x448)$/.exec(metoda); if (!k) return null; return /^ed/.test(k[1]) ? { grup: 'EDDSA', nume: k[1] === 'ed448' ? 'Ed448' : 'Ed25519', functii: ['keygen'] } : { grup: 'XDH', nume: k[1] === 'x448' ? 'X448' : 'X25519', functii: ['keygen'] }; }
if (/^(SslConnector|SslAcceptor|SslContext)$/.test(tip) && /^(builder|mozilla_modern|mozilla_intermediate|mozilla_modern_v5|mozilla_intermediate_v5)$/.test(metoda)) return { grup: 'TLS', param: 'negotiated', nume: `openssl ${tip}` }; if (/^(SslConnector|SslAcceptor|SslContext)$/.test(tip) && /^(builder|mozilla_modern|mozilla_intermediate|mozilla_modern_v5|mozilla_intermediate_v5)$/.test(metoda)) return { grup: 'TLS', param: 'negotiated', nume: `${NUME_CARGO(crate)} ${tip}` };
return null; return null;
} }
case 'rustls': case 'tokio_rustls': case 'hyper_rustls': case 'native_tls': case 'rustls': case 'tokio_rustls': case 'hyper_rustls': case 'native_tls':
@ -503,8 +529,12 @@ function activStatic(r, metoda, generic, ctx, m) {
if (tip === 'TorClient') return { grup: 'UNKNOWN', nume: 'Tor client (arti)', motiv: 'Tor circuit and channel cryptography is negotiated with the relays by the Tor protocol version in use, not visible in this file.' }; if (tip === 'TorClient') return { grup: 'UNKNOWN', nume: 'Tor client (arti)', motiv: 'Tor circuit and channel cryptography is negotiated with the relays by the Tor protocol version in use, not visible in this file.' };
return null; return null;
case 'libcrux_ml_kem': { case 'libcrux_ml_kem': {
const f = fn(/generate/.test(metoda) ? 'keygen' : /encaps/.test(metoda) ? 'encapsulate' : /decaps/.test(metoda) ? 'decapsulate' : 'other');
// 2026-10-18 (libsignal): modulele kyber512/768/1024 ale lui libcrux sunt Kyber pre-standard (prekey-urile PQXDH de dinaintea ML-KEM)
const ky = cale.map((s) => /^kyber(512|768|1024)$/.exec(s)).find(Boolean);
if (ky) return { grup: 'PREPQ', nume: `Kyber-${ky[1]}`, primitiv: 'kem', functii: f, nota: 'libcrux kyber module: CRYSTALS-Kyber, the pre-standard form of ML-KEM.' };
const k = /MlKem(512|768|1024)/.exec(tip) || /mlkem(512|768|1024)/.exec(cale.join('::')); const k = /MlKem(512|768|1024)/.exec(tip) || /mlkem(512|768|1024)/.exec(cale.join('::'));
return k ? { grup: 'MLKEM', param: k[1], functii: fn(/generate/.test(metoda) ? 'keygen' : /encaps/.test(metoda) ? 'encapsulate' : /decaps/.test(metoda) ? 'decapsulate' : 'other') } : null; return k ? { grup: 'MLKEM', param: k[1], functii: f } : null;
} }
case 'libcrux_ml_dsa': { case 'libcrux_ml_dsa': {
const k = /MLDSA(44|65|87)|ml_dsa_(44|65|87)/.exec(tip + ' ' + cale.join('::')); const k = /MLDSA(44|65|87)|ml_dsa_(44|65|87)/.exec(tip + ' ' + cale.join('::'));
@ -539,10 +569,16 @@ function activFunctie(cale, generic, ctx, m) {
if (fnume === 'default_provider' && modul === 'aws_lc_rs') return { grup: 'UNKNOWN', nume: 'rustls aws-lc-rs provider', motiv: 'rustls with the aws-lc-rs provider: X25519MLKEM768 is offered by default from rustls 0.23.22 (and only with the prefer-post-quantum feature before 0.23.22); the rustls version is in Cargo.lock, not here.' }; if (fnume === 'default_provider' && modul === 'aws_lc_rs') return { grup: 'UNKNOWN', nume: 'rustls aws-lc-rs provider', motiv: 'rustls with the aws-lc-rs provider: X25519MLKEM768 is offered by default from rustls 0.23.22 (and only with the prefer-post-quantum feature before 0.23.22); the rustls version is in Cargo.lock, not here.' };
return null; return null;
case 'libcrux_ml_kem': { case 'libcrux_ml_kem': {
if (!/^(generate_key_pair|encapsulate|decapsulate|validate_public_key|validate_private_key)$/.test(fnume)) return null;
const f = [/generate/.test(fnume) ? 'keygen' : /encaps/.test(fnume) ? 'encapsulate' : /decaps/.test(fnume) ? 'decapsulate' : 'other'];
const ky = /^kyber(512|768|1024)$/.exec(modul || '');
if (ky) return { grup: 'PREPQ', nume: `Kyber-${ky[1]}`, primitiv: 'kem', functii: f, nota: 'libcrux kyber module: CRYSTALS-Kyber, the pre-standard form of ML-KEM.' };
const k = /^mlkem(512|768|1024)$/.exec(modul || ''); const k = /^mlkem(512|768|1024)$/.exec(modul || '');
if (!k || !/^(generate_key_pair|encapsulate|decapsulate|validate_public_key|validate_private_key)$/.test(fnume)) return null; return k ? { grup: 'MLKEM', param: k[1], functii: f } : null;
return { grup: 'MLKEM', param: k[1], functii: [/generate/.test(fnume) ? 'keygen' : /encaps/.test(fnume) ? 'encapsulate' : /decaps/.test(fnume) ? 'decapsulate' : 'other'] };
} }
case 'x25519_dalek':
// 2026-10-18 (libsignal): functia libera x25519(secret, point), nu numai tipurile StaticSecret/EphemeralSecret
return fnume === 'x25519' ? { grup: 'XDH', nume: 'X25519', functii: ['other'] } : null;
case 'libcrux_ml_dsa': { case 'libcrux_ml_dsa': {
const k = /^ml_dsa_(44|65|87)$/.exec(modul || ''); const k = /^ml_dsa_(44|65|87)$/.exec(modul || '');
if (!k || !/^(generate_key_pair|sign|verify|sign_pre_hashed_shake128|verify_pre_hashed_shake128)$/.test(fnume)) return null; if (!k || !/^(generate_key_pair|sign|verify|sign_pre_hashed_shake128|verify_pre_hashed_shake128)$/.test(fnume)) return null;

View File

@ -32,6 +32,38 @@ const MUTATII = [
tinta: 'rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey)', tinta: 'rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey)',
martor: B, martor: B,
}, },
{
nume: 'API-ul libsignal Java recunoscut fara ancora de import/pachet (orice ECKeyPair.generate devine libsignal)',
fisier: 'lib/detect-java.mjs',
ancora: 'if (!ancore.length) return;',
cu: 'if (false) return;',
tinta: 'negativ: numele API-ului libsignal fara importul lui nu produc gasiri (Java si TypeScript), iar cu import se recunosc',
martor: B,
},
{
nume: 'pachetul @signalapp/libsignal-client nu mai e recunoscut (JS)',
fisier: 'lib/detect-js.mjs',
ancora: "const lsc = importa(/^@signalapp\\/libsignal-client(\\/.*)?$/);",
cu: 'const lsc = [];',
tinta: 'pozitiv: js/libsignal-client.ts',
martor: B,
},
{
nume: 'modulele kyber ale lui libcrux citite ca ML-KEM (Rust)',
fisier: 'lib/detect-rust.mjs',
ancora: "const ky = /^kyber(512|768|1024)$/.exec(modul || '');",
cu: 'const ky = null;',
tinta: 'pozitiv: rust/libsignal.rs',
martor: B,
},
{
nume: 'tiparul Noise hibrid (hfs) citit ca X25519 singur (Rust)',
fisier: 'lib/detect-rust.mjs',
ancora: 'if (m[3]) {',
cu: 'if (false) {',
tinta: 'pozitiv: rust/libsignal.rs',
martor: B,
},
{ {
nume: 'Cargo.toml scos din manifeste', nume: 'Cargo.toml scos din manifeste',
fisier: 'lib/manifeste.mjs', fisier: 'lib/manifeste.mjs',

View File

@ -53,8 +53,14 @@ try {
if (rustN || cargoN) console.log(` (in afara echivalentei: ${rustN} gasiri Rust, ${cargoN} biblioteci Cargo; 0.1.0 nu le stia)`); if (rustN || cargoN) console.log(` (in afara echivalentei: ${rustN} gasiri Rust, ${cargoN} biblioteci Cargo; 0.1.0 nu le stia)`);
const ka = a.findings.map(norm), kb = b.findings.map(norm); const ka = a.findings.map(norm), kb = b.findings.map(norm);
const sa = new Set(ka), sb = new Set(kb); const sa = new Set(ka), sb = new Set(kb);
const doarV = ka.filter((x) => !sb.has(x)), doarN = kb.filter((x) => !sa.has(x)); // 0.3.0 (2026-10-18, libsignal): detectoarele noi pe limbaje pe care 0.1.0 le STIA (API-ul libsignal in Java si Node) sunt adaugiri,
const ok = !doarV.length && !doarN.length && ka.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries); // nu rescrieri: o gasire numai in versiunea noua, cu un API din lista de mai jos, se numara si se spune, nu e diferenta. O gasire
// numai in 0.1.0 ramane intotdeauna diferenta: echivalenta pazeste ca rescrierea nu a PIERDUT nimic.
const ADAUGIRI = /"api":"(?:(?:ECKeyPair|IdentityKeyPair|ECPrivateKey|PrivateKey|KEMKeyPair)\.generate|Curve\.generateKeyPair|HKDF\.deriveSecrets|new (?:Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32|PreKeyBundle)|import org\.signal\.libsignal|hkdf|Aes256GcmSiv\.new|PreKeyBundle\.new|processPreKeyBundle|import @signalapp\/libsignal-client(?:\/[^"]*)?)"/;
const doarV = ka.filter((x) => !sb.has(x)), doarN0 = kb.filter((x) => !sa.has(x));
const adaugate = doarN0.filter((x) => ADAUGIRI.test(x)), doarN = doarN0.filter((x) => !ADAUGIRI.test(x));
if (adaugate.length) console.log(` (in afara echivalentei: ${adaugate.length} gasiri ale detectoarelor adaugate dupa 0.1.0, API-ul libsignal Java/Node)`);
const ok = !doarV.length && !doarN.length && ka.length + adaugate.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries);
comparate += ka.length; comparate += ka.length;
if (!ok) dif++; if (!ok) dif++;
console.log(`${ok ? 'IDENTIC ' : 'DIFERIT '} ${d}: ${a.stats.filesSeen} fisiere, gasiri ${ka.length}/${kb.length}, biblioteci ${a.libraries.length}/${b.libraries.length}`); console.log(`${ok ? 'IDENTIC ' : 'DIFERIT '} ${d}: ${a.stats.filesSeen} fisiere, gasiri ${ka.length}/${kb.length}, biblioteci ${a.libraries.length}/${b.libraries.length}`);

View File

@ -0,0 +1,27 @@
// Fixtura: API-ul Java/Android al libsignal (org.signal.libsignal), 2026-10-18. Fisierul nu se compileaza; e doar text pentru scaner.
// Ancora e importul org.signal.libsignal.*; fara el, numele scurte (ECKeyPair, HKDF, Aes256GcmSiv) nu inseamna libsignal.
package fixturi;
import org.signal.libsignal.protocol.IdentityKeyPair;
import org.signal.libsignal.protocol.ecc.ECKeyPair;
import org.signal.libsignal.protocol.kem.KEMKeyPair;
import org.signal.libsignal.protocol.kem.KEMKeyType;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.state.PreKeyBundle;
import org.signal.libsignal.crypto.Aes256GcmSiv;
import org.signal.libsignal.crypto.Aes256GcmEncryption;
import org.signal.libsignal.crypto.Aes256Ctr32;
public class Libsignal {
public void exemple(byte[] key, byte[] nonce, byte[] ikm, KEMKeyType tipDinParametru) throws Exception {
ECKeyPair ec = ECKeyPair.generate(); // EXPECT: X25519 | quantum-vulnerable
IdentityKeyPair id = IdentityKeyPair.generate(); // EXPECT: X25519 | quantum-vulnerable
KEMKeyPair kem = KEMKeyPair.generate(KEMKeyType.KYBER_1024); // EXPECT: Kyber-1024 | quantum-safe
KEMKeyPair kem2 = KEMKeyPair.generate(tipDinParametru); // EXPECT: libsignal KEM key pair | unknown
byte[] okm = HKDF.deriveSecrets(ikm, "info".getBytes(), 32); // EXPECT: HKDF | quantum-safe
Aes256GcmSiv siv = new Aes256GcmSiv(key); // EXPECT: AES-256-GCM-SIV | quantum-safe
Aes256GcmEncryption gcm = new Aes256GcmEncryption(key, nonce, new byte[0]); // EXPECT: AES-256-GCM | quantum-safe
Aes256Ctr32 ctr = new Aes256Ctr32(key, nonce, 0); // EXPECT: AES-256-CTR | quantum-safe
PreKeyBundle bundle = new PreKeyBundle(1, 1, 2, ec.getPublicKey(), 3, ec.getPublicKey(), new byte[64], id.getPublicKey()); // EXPECT: libsignal session setup (X3DH/PQXDH) | unknown
}
}

View File

@ -0,0 +1,12 @@
// Fixtura: un fisier care importa libsignal fara niciun apel recunoscut: importul e dovada slaba (unknown), o singura data.
// Si un fisier FARA importul libsignal nu primeste nimic pentru aceleasi nume scurte (vezi negative/FaraLibsignal.java).
package fixturi;
import org.signal.libsignal.protocol.SessionCipher; // EXPECT: libsignal (org.signal.libsignal) | unknown
import org.signal.libsignal.protocol.SignalProtocolAddress;
public class LibsignalImport {
public Object cifru(Object store, SignalProtocolAddress adresa) {
return new SessionCipher(store, adresa);
}
}

View File

@ -0,0 +1,6 @@
// Fixtura: importul pachetului fara niciun apel recunoscut e dovada slaba (unknown), o singura data pe fisier.
import { SessionCipher, ProtocolAddress } from '@signalapp/libsignal-client'; // EXPECT: @signalapp/libsignal-client | unknown
export function adresa(nume: string) {
return { cipher: SessionCipher, address: ProtocolAddress.new(nume, 1) };
}

View File

@ -0,0 +1,14 @@
// Fixtura: un client al bibliotecii Signal pentru Node (@signalapp/libsignal-client), 2026-10-18. Ancora e importul pachetului.
import * as SignalClient from '@signalapp/libsignal-client';
import { PrivateKey, KEMKeyPair, hkdf, Aes256GcmSiv } from '@signalapp/libsignal-client';
export function chei(ikm: Buffer, key: Buffer) {
const priv = PrivateKey.generate(); // EXPECT: X25519 | quantum-vulnerable
const id = SignalClient.IdentityKeyPair.generate(); // EXPECT: X25519 | quantum-vulnerable
const kem = KEMKeyPair.generate(); // EXPECT: Kyber-1024 | quantum-safe
const okm = hkdf(32, ikm, Buffer.from('info'), null); // EXPECT: HKDF | quantum-safe
const okm2 = SignalClient.hkdf(32, ikm, Buffer.from('info'), null); // EXPECT: HKDF | quantum-safe
const siv = Aes256GcmSiv.new(key); // EXPECT: AES-256-GCM-SIV | quantum-safe
const bundle = SignalClient.PreKeyBundle.new(1, 1, 2, priv.getPublicKey(), 3, priv.getPublicKey(), Buffer.alloc(64), id.publicKey); // EXPECT: libsignal session setup (X3DH/PQXDH) | unknown
return { priv, id, kem, okm, okm2, siv, bundle };
}

View File

@ -0,0 +1,15 @@
// Fixtura negativa: aceleasi nume scurte ca in API-ul libsignal (ECKeyPair.generate, HKDF.deriveSecrets, new Aes256GcmSiv), dar FARA
// importul org.signal.libsignal: nu sunt libsignal, deci nu produc nicio gasire. Fisierul nu se compileaza; e doar text pentru scaner.
package fixturi;
import com.exemplu.chei.ECKeyPair;
import com.exemplu.kdf.HKDF;
import com.exemplu.cifru.Aes256GcmSiv;
public class FaraLibsignal {
public Object exemple(byte[] key, byte[] ikm) {
ECKeyPair ec = ECKeyPair.generate();
byte[] okm = HKDF.deriveSecrets(ikm, "info".getBytes(), 32);
return new Aes256GcmSiv(key);
}
}

View File

@ -0,0 +1,10 @@
// Fixtura negativa: aceleasi nume scurte ca in API-ul @signalapp/libsignal-client (PrivateKey.generate, KEMKeyPair.generate, hkdf,
// Aes256GcmSiv.new), dar importate din alta parte: nu sunt libsignal, deci nu produc nicio gasire.
import { PrivateKey, KEMKeyPair, hkdf, Aes256GcmSiv } from '../chei-proprii';
export function chei(ikm: Buffer, key: Buffer) {
const priv = PrivateKey.generate();
const kem = KEMKeyPair.generate();
const okm = hkdf(32, ikm, Buffer.from('info'), null);
return { priv, kem, okm, siv: Aes256GcmSiv.new(key) };
}

View File

@ -0,0 +1,30 @@
// Fixtura: ce a scapat inventarului pe libsignal (2026-10-18): BoringSSL prin `boring` / `boring-signal` (stiva TLS a lui libsignal,
// acelasi API ca openssl), HmacReset din hmac 0.13, tipul de mod adus prin `use cbc::Decryptor`, functia libera x25519(), modulele
// kyber ale lui libcrux (Kyber pre-standard) si tiparul Noise hibrid hfs (X25519 + Kyber1024 in aceeasi strangere de mana).
use boring_signal::ssl::{SslConnector, SslMethod, SslVersion};
use boring::rsa::Rsa;
use boring::hash::MessageDigest;
use hmac::{HmacReset, Mac};
use sha2::Sha256;
use aes::Aes256;
use cbc::Decryptor;
use cbc::cipher::KeyIvInit;
use x25519_dalek as x25519;
use libcrux_ml_kem::kyber1024;
use libcrux_ml_kem::mlkem768;
use snow::Builder;
pub const NOISE_HFS: &str = "Noise_NKhfs_25519+Kyber1024_ChaChaPoly_SHA256"; // EXPECT: X25519+Kyber1024 (Noise hfs) | quantum-safe ; ChaCha20-Poly1305 | quantum-safe ; SHA-256 | quantum-safe
pub fn exemple(key: &[u8; 32], iv: &[u8; 16], secret: [u8; 32], randomness: [u8; 64]) {
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); // EXPECT: boring-signal SslConnector | unknown
ssl.set_min_proto_version(Some(SslVersion::TLS1_3)).unwrap(); // EXPECT: TLSv1.3 | unknown
let rsa = Rsa::generate(2048).unwrap(); // EXPECT: RSA-2048 | quantum-vulnerable
let md = MessageDigest::sha256(); // EXPECT: SHA-256 | quantum-safe
let mac = HmacReset::<Sha256>::new_from_slice(key).unwrap(); // EXPECT: HMAC-SHA-256 | quantum-safe
let dec = Decryptor::<Aes256>::new(key.into(), iv.into()); // EXPECT: AES-256-CBC | quantum-safe
let pk = x25519::x25519(secret, x25519::X25519_BASEPOINT_BYTES); // EXPECT: X25519 | quantum-vulnerable
let kp = kyber1024::generate_key_pair(randomness); // EXPECT: Kyber-1024 | quantum-safe
let kp2 = mlkem768::generate_key_pair(randomness); // EXPECT: ML-KEM-768 | quantum-safe
let _ = (ssl, rsa, md, mac, dec, pk, kp, kp2, Builder::new(NOISE_HFS.parse().unwrap()));
}

View File

@ -18,8 +18,8 @@ const lx = await import(pathToFileURL(join(dirname(CALE_MODUL), 'lib', 'lexer.mj
const CA_JSON = process.argv.includes('--json'); const CA_JSON = process.argv.includes('--json');
// numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient // numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient
const MARCAJE_ASTEPTATE = 128; const MARCAJE_ASTEPTATE = 158;
const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'python/crypto_py.py', 'java/Crypto.java', 'go/crypto.go', 'rust/cripto.rs', 'rust/ambiguu.rs', 'rust/glob.rs', 'negative/variabile.js']; const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'js/libsignal-client.ts', 'js/libsignal-client-import.ts', 'python/crypto_py.py', 'java/Crypto.java', 'java/Libsignal.java', 'java/LibsignalImport.java', 'go/crypto.go', 'rust/cripto.rs', 'rust/ambiguu.rs', 'rust/glob.rs', 'rust/libsignal.rs', 'negative/variabile.js'];
const probe = []; const probe = [];
const proba = (nume, fn) => probe.push({ nume, fn }); const proba = (nume, fn) => probe.push({ nume, fn });
@ -254,11 +254,26 @@ proba('rust: Cargo.toml: forma tabel, workspace = true fara versiune, redenumire
proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => { proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => {
const rez = scan(join(FIX, 'negative')); const rez = scan(join(FIX, 'negative'));
asigura(rez.stats.filesSeen === 8 && rez.stats.codeFilesTotal === 8, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`); asigura(rez.stats.filesSeen === 10 && rez.stats.codeFilesTotal === 10, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`);
const g = gasiriDin(rez, 'fara-cripto.js'); const g = gasiriDin(rez, 'fara-cripto.js');
asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`); asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`);
}); });
// 2026-10-18 (libsignal): API-ul libsignal se recunoaste numai cu ancora lui (importul org.signal.libsignal / @signalapp/libsignal-client)
proba('negativ: numele API-ului libsignal fara importul lui nu produc gasiri (Java si TypeScript), iar cu import se recunosc', () => {
const rez = scan(FIX);
asigura(gasiriDin(rez, 'java/Libsignal.java').some((x) => x.name === 'Kyber-1024') && gasiriDin(rez, 'js/libsignal-client.ts').some((x) => x.name === 'Kyber-1024'), 'controlul pozitiv: Kyber-1024 din fixturile libsignal lipseste');
const g = [...gasiriDin(rez, 'negative/FaraLibsignal.java'), ...gasiriDin(rez, 'negative/fara-libsignal.ts')];
asigura(g.length === 0, `fara ancora au produs: ${g.map((x) => `${x.file}:${x.line} ${x.name}`).join(', ')}`);
// ancora de pachet: un fisier din chiar arborele libsignal (package org.signal.libsignal...) nu are import, dar e libsignal
const d = mkdtempSync(join(tmpdir(), 'ci-libsignal-'));
try {
writeFileSync(join(d, 'ECKeyPairTest.java'), 'package org.signal.libsignal.protocol.ecc;\n\npublic class ECKeyPairTest {\n void t() { ECKeyPair kp = ECKeyPair.generate(); }\n}\n');
const r = inv.scan(d);
asigura(r.findings.length === 1 && r.findings[0].name === 'X25519' && r.findings[0].line === 4, `ancora de pachet: ${JSON.stringify(r.findings.map((x) => [x.line, x.name]))}`);
} finally { rmSync(d, { recursive: true, force: true }); }
});
proba('negativ: algoritm din variabila reasignata sau din parametru iese unknown, nu ghicit', () => { proba('negativ: algoritm din variabila reasignata sau din parametru iese unknown, nu ghicit', () => {
comparaExact('negative/variabile.js'); comparaExact('negative/variabile.js');
const g = gasiriDin(scan(FIX), 'negative/variabile.js'); const g = gasiriDin(scan(FIX), 'negative/variabile.js');

View File

@ -1,5 +1,6 @@
# Aere Cloud, Quantum Security Gateway. Imagine minima: Node 24 si un singur fisier, fara nicio dependinta. # Aere Cloud, Quantum Security Gateway. Imagine minima: Node 24 si un singur fisier, fara nicio dependinta.
# NECONSTRUITA si netrasa pe laptopul de dezvoltare (2026-09-25): versiunea OpenSSL din imagine e NEMASURATA aici. # Construita si masurata 2026-10-18 pe rpc2 din octetii comisi (dovezi/imagine-docker-2026-10-18.md): 62,5 MB, Node v24.22.0 cu
# OpenSSL 3.5.8, handshake X25519MLKEM768 reusit, clientul clasic refuzat in hybrid-only. Pe laptopul de dezvoltare nu e docker.
# Gateway-ul o verifica singur la pornire (refuza sub OpenSSL 3.5) si o scrie in randul 'listening' din jurnal. # Gateway-ul o verifica singur la pornire (refuza sub OpenSSL 3.5) si o scrie in randul 'listening' din jurnal.
# #
# docker build -t aere-pq-gateway . # docker build -t aere-pq-gateway .

View File

@ -84,8 +84,10 @@ docker run --rm -p 443:8443 \
The mounted private key must be readable by the container user (uid 1000), otherwise the gateway refuses to start The mounted private key must be readable by the container user (uid 1000), otherwise the gateway refuses to start
with `cannot read KEY (EACCES)`. The OpenSSL version inside the image is printed on the `listening` line; the gateway with `cannot read KEY (EACCES)`. The OpenSSL version inside the image is printed on the `listening` line; the gateway
refuses to start if it is older than 3.5. Not yet measured by us: a build of this image and the OpenSSL version it refuses to start if it is older than 3.5. Measured on 2026-10-18 from the committed bytes (`dovezi/imagine-docker-2026-10-18.md`):
carries (the test suite runs the gateway directly on Node.js 24 with OpenSSL 3.5.5). the image builds in 6 s (62.5 MB, `node:24-alpine`, runs as `node`), its base carries Node v24.22.0 with OpenSSL 3.5.8, a client
offering only X25519MLKEM768 completes TLS 1.3 against it, a classical-only client is refused in `hybrid-only` and accepted in
`hybrid-preferred` (where the hybrid group is chosen when offered). The test suite still runs the gateway directly on Node.js 24.
## Configuration ## Configuration

View File

@ -0,0 +1,27 @@
# Imaginea Docker a gateway-ului, construita si masurata, 2026-10-18 03:31Z
Pana azi README-ul spunea "not yet measured by us: a build of this image and the OpenSSL version it carries", iar roadmap-ul (s.2a,
Shield, punctul 2) o tinea NEMASURATA. Masurat pe rpc2 (docker 29.6.2, 2 nuclee), din octetii COMISI (`git archive HEAD
cloud-gateway/pq-gateway/Dockerfile cloud-gateway/pq-gateway/pq-gateway.mjs`; pq-gateway.mjs sha256 `8602ac1afa2fe002...`,
Dockerfile `9690e8d1df1a9dc2...`), cu scriptul `pqgw-imagine-rpc2.sh` si sonda `pqgw-sonda.mjs` (ambele pastrate langa acest fisier):
| ce | masurat |
|---|---|
| `docker build` | cod 0 in 6 s; imaginea `sha256:d1e6b588a74a...`, 62.467.140 de octeti, linux/amd64, `user=node` |
| baza `node:24-alpine` (digest `sha256:44fce696f38b...`) | Node v24.22.0, **OpenSSL 3.5.8** (`docker run --rm node:24-alpine node -p process.versions.openssl`) |
| pornirea, `hybrid-only` | containerul `Up`; randul `listening`: `mode hybrid-only, groups X25519MLKEM768, minTlsVersion TLSv1.3`, plus avertismentul ca amontele e http simplu |
| client care ofera NUMAI `X25519MLKEM768` (Node 24, OpenSSL 3.5.8, de pe aceeasi gazda) | **handshake reusit**: TLSv1.3, TLS_AES_256_GCM_SHA384, grupul raportat `X25519MLKEM768`, 16 ms |
| client care ofera NUMAI `X25519`, in `hybrid-only` (control negativ) | **refuzat**: `ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE`, 18 ms |
| `hybrid-preferred`: client numai `X25519` | reusit (TLSv1.3, grup `X25519`) |
| `hybrid-preferred`: client `X25519MLKEM768:X25519` | reusit, grupul ales `X25519MLKEM768` (hibridul e preferat) |
| RPC-ul public local al lui rpc2 inainte / dupa | 9 ms / 10 ms (`eth_blockNumber`) |
| curatenie | containerul, imaginea gateway-ului si imaginea de baza sterse; 0 containere, 0 imagini straine ramase; porturile 8444/8081 libere |
Certificatul probei: EC P-256 autosemnat, 2 zile, citibil de uid 1000 (`chmod 644`); amontele probei: `python3 -m http.server` pe
127.0.0.1:8081. Cererea HTTP prin gateway cu `curl --curves X25519MLKEM768` NU s-a masurat (optiunea `%{tls_version}` lipseste in
curl-ul gazdei; nu e o constatare despre gateway).
Doua capcane platite in aceeasi rulare, fara paguba: (1) randul `listening` a fost taiat la 220 de coloane de `cut`, deci versiunea
OpenSSL a gateway-ului a lipsit din jurnalul adus si s-a masurat separat pe imaginea de baza; (2) serverul de proba pornit cu
`nohup ... &` dintr-un subshell a ramas in viata dupa script (`$!` scris intr-un fisier relativ sters de curatenie), a tinut sesiunea
ssh deschisa 10 minute dupa ce scriptul se terminase (aceeasi familie cu C5, 2026-10-11) si a fost oprit de mana; nimic nu a ramas armat.

View File

@ -0,0 +1,34 @@
#!/bin/bash
# pqgw-imagine-rpc2.sh (PE rpc2; fisierele vin in /root/pqgw-imagine: pqgw.tar din `git archive HEAD` + pqgw-sonda.mjs):
# masoara ce README-ul gateway-ului numeste NEMASURAT: imaginea Docker se construieste, PORNESTE, poarta OpenSSL >= 3.5, si un client
# care ofera NUMAI X25519MLKEM768 face handshake, iar unul clasic e REFUZAT in modul hybrid-only (controlul negativ). Totul pe 127.0.0.1,
# port 8444, container si imagine sterse la sfarsit (nimic ramane armat). RPC-ul local e cronometrat inainte si dupa.
set -u
D=/root/pqgw-imagine; cd "$D" || exit 2
N=/opt/node24/bin/node
rpc() { local t0=$(date +%s%N); curl -s -m 5 -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:8545 | grep -o '"result":"0x[0-9a-f]*"' | head -1; echo " ($(( ($(date +%s%N) - t0) / 1000000 )) ms)"; }
echo "== RPC inainte: $(rpc)"
rm -rf src && mkdir -p src certs && tar -xf pqgw.tar -C src && ls -la src/cloud-gateway/pq-gateway | sed -n '2,5p'
sha256sum src/cloud-gateway/pq-gateway/pq-gateway.mjs src/cloud-gateway/pq-gateway/Dockerfile | cut -c1-16,66-
echo "== docker build"; t0=$(date +%s); docker build -q -t aere-pq-gateway:proba src/cloud-gateway/pq-gateway > build.log 2>&1; c=$?; echo "build cod=$c in $(( $(date +%s) - t0 )) s; $(tail -1 build.log | cut -c1-100)"
[ $c -eq 0 ] || { tail -20 build.log; exit 1; }
docker image inspect aere-pq-gateway:proba --format 'imagine: {{.Id}} {{.Size}} octeti, {{.Os}}/{{.Architecture}}, user={{.Config.User}}'
echo "== certificat de proba (EC P-256, autosemnat, citibil de uid 1000)"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout certs/privkey.pem -out certs/fullchain.pem -days 2 -subj '/CN=proba.local' > /dev/null 2>&1 && chmod 644 certs/privkey.pem certs/fullchain.pem && echo "cert ok"
echo "== upstream de proba (python http.server pe 127.0.0.1:8081)"; (cd certs && nohup python3 -m http.server 8081 --bind 127.0.0.1 > /dev/null 2>&1 & echo $! > ../upstream.pid); sleep 1
docker rm -f pqgw-proba > /dev/null 2>&1
echo "== pornire: hybrid-only"
docker run -d --name pqgw-proba --network host -e AERE_PQGW_LISTEN=127.0.0.1:8444 -e AERE_PQGW_MODE=hybrid-only -e AERE_PQGW_UPSTREAM=http://127.0.0.1:8081 -v "$D/certs:/etc/aere-pq-gateway:ro" aere-pq-gateway:proba > /dev/null; sleep 3
docker ps --filter name=pqgw-proba --format 'container: {{.Status}}'
echo "-- jurnal (randul listening cu OpenSSL)"; docker logs pqgw-proba 2>&1 | head -5 | cut -c1-220
echo "-- sonda hibrid singur (trebuie ok)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519MLKEM768; echo "cod=$?"
echo "-- sonda clasic singur in hybrid-only (CONTROL NEGATIV: trebuie refuzat)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519; echo "cod=$?"
echo "-- o cerere HTTP prin gateway (hibrid) spre upstream"; curl -sk -m 8 --curves X25519MLKEM768 --tlsv1.3 https://127.0.0.1:8444/ -o /dev/null -w 'http=%{http_code} ssl=%{ssl_verify_result} tls=%{tls_version}\n' 2>&1 | head -1 || true
echo "== repornire: hybrid-preferred (clasicul trebuie sa mearga, hibridul preferat)"
docker rm -f pqgw-proba > /dev/null 2>&1
docker run -d --name pqgw-proba --network host -e AERE_PQGW_LISTEN=127.0.0.1:8444 -e AERE_PQGW_MODE=hybrid-preferred -e AERE_PQGW_UPSTREAM=http://127.0.0.1:8081 -v "$D/certs:/etc/aere-pq-gateway:ro" aere-pq-gateway:proba > /dev/null; sleep 3
echo "-- sonda clasic singur in hybrid-preferred (trebuie ok)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519; echo "cod=$?"
echo "-- sonda hibrid+clasic in hybrid-preferred (trebuie ok)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519MLKEM768:X25519; echo "cod=$?"
docker logs pqgw-proba 2>&1 | tail -4 | cut -c1-220
echo "== curatenie"; docker rm -f pqgw-proba > /dev/null 2>&1; docker rmi aere-pq-gateway:proba > /dev/null 2>&1 && echo "imaginea stearsa"; kill "$(cat upstream.pid)" 2>/dev/null; rm -rf certs upstream.pid; docker ps --filter name=pqgw --format '{{.Names}}' | grep -c . | sed 's/^/containere pqgw ramase: /'
echo "== RPC dupa: $(rpc)"

View File

@ -0,0 +1,14 @@
// pqgw-sonda.mjs <gazda> <port> <grupuri>: o strangere de mana TLS 1.3 reala cu grupurile de schimb de cheie date (de ex.
// X25519MLKEM768 singur, sau X25519 singur), fara verificarea certificatului (cel al probei e autosemnat). Tipareste JSON cu
// protocolul, cifrul, grupul raportat de OpenSSL (pentru hibrid OpenSSL nu il numeste; reusita cu grupul hibrid OFERIT SINGUR e dovada)
// si motivul esecului. Iesire 0 = handshake reusit, 1 = refuzat.
import tls from 'node:tls';
const [gazda, port, grupuri] = process.argv.slice(2);
const t0 = Date.now();
const s = tls.connect({ host: gazda, port: Number(port), servername: 'proba.local', rejectUnauthorized: false, minVersion: 'TLSv1.3', maxVersion: 'TLSv1.3', ecdhCurve: grupuri, ALPNProtocols: ['http/1.1'] }, () => {
const ek = s.getEphemeralKeyInfo();
console.log(JSON.stringify({ ok: true, grupuriOferite: grupuri, protocol: s.getProtocol(), cifru: s.getCipher().name, grupRaportat: ek && ek.name, alpn: s.alpnProtocol, ms: Date.now() - t0, openssl: process.versions.openssl }));
s.end(); process.exitCode = 0;
});
s.setTimeout(8000, () => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, motiv: 'timeout' })); s.destroy(); process.exitCode = 1; });
s.on('error', (e) => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, motiv: e.code || e.message, ms: Date.now() - t0, openssl: process.versions.openssl })); process.exitCode = 1; });