diff --git a/README.md b/README.md index 0eef780..ff9548d 100644 --- a/README.md +++ b/README.md @@ -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-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`) | -| 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= 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 | | 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`) | | 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 `) | 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) | @@ -62,4 +62,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## 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). diff --git a/control-plane/README.md b/control-plane/README.md index 27d957a..92f6314 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -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. { 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 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 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 -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` diff --git a/control-plane/control-plane.mjs b/control-plane/control-plane.mjs index 406409a..8426b13 100644 --- a/control-plane/control-plane.mjs +++ b/control-plane/control-plane.mjs @@ -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) // 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, + // 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, }; } +let scopulCaii = () => 'production'; async function main() { const args = process.argv.slice(2); @@ -56,7 +59,9 @@ async function main() { if (!code) { console.error('usage: node control-plane.mjs --code [--scan scan.json] [--json]'); process.exit(2); } 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); } 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); } 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(` 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) { 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 : ''}`); diff --git a/control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md b/control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md new file mode 100644 index 0000000..f662ed5 --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md @@ -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`. diff --git a/control-plane/dovezi/retete-2026-10-18/go/main.go b/control-plane/dovezi/retete-2026-10-18/go/main.go new file mode 100644 index 0000000..dde3eae --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/go/main.go @@ -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:, 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("", "")) +} diff --git a/control-plane/dovezi/retete-2026-10-18/masoara-go.sh b/control-plane/dovezi/retete-2026-10-18/masoara-go.sh new file mode 100644 index 0000000..d679458 --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/masoara-go.sh @@ -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 diff --git a/control-plane/dovezi/retete-2026-10-18/masoara-nginx-rpc2.sh b/control-plane/dovezi/retete-2026-10-18/masoara-nginx-rpc2.sh new file mode 100644 index 0000000..2482efd --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/masoara-nginx-rpc2.sh @@ -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 diff --git a/control-plane/dovezi/retete-2026-10-18/scaneaza-local.mjs b/control-plane/dovezi/retete-2026-10-18/scaneaza-local.mjs new file mode 100644 index 0000000..eba8c51 --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/scaneaza-local.mjs @@ -0,0 +1,8 @@ +// scaneaza-local.mjs : 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, ' ')); diff --git a/control-plane/dovezi/retete-2026-10-18/tls-sonda.mjs b/control-plane/dovezi/retete-2026-10-18/tls-sonda.mjs new file mode 100644 index 0000000..01bae65 --- /dev/null +++ b/control-plane/dovezi/retete-2026-10-18/tls-sonda.mjs @@ -0,0 +1,15 @@ +// tls-sonda.mjs [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; }); diff --git a/control-plane/plan-migrare.mjs b/control-plane/plan-migrare.mjs index 4c3aef2..c6a9b0e 100644 --- a/control-plane/plan-migrare.mjs +++ b/control-plane/plan-migrare.mjs @@ -89,6 +89,36 @@ function cnDin(subjectName) { 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) { if (!vulnerabil(f)) return null; const prim = primitivaDe(f); @@ -105,11 +135,15 @@ function actiuneDinInventar(f) { ? `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 + ')' : ''}`; // 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 blocker = null; let cn; 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); if (c && NUME_GAZDA.test(c)) cn = c.toLowerCase(); 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)'; } 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 { ref: f.ref || `${f.assetType || 'algorithm'}:${(f.name || prim)}`, asset: numeAsset, @@ -131,7 +168,8 @@ function actiuneDinInventar(f) { method, product: method === 'auto-aere' ? t.product : null, how, ...(cn ? { cn } : {}), primitive: prim, - urgency: urgenta(prim, false, certLunga), blocker, + urgency, blocker, + scope, location: f.location || null, source: 'inventory', }; @@ -220,6 +258,8 @@ export function planeaza({ inventory = [], scan = null } = {}) { manual: dedup.filter((a) => a.method === 'manual').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), {}), + // 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 }; } diff --git a/control-plane/proba-control-plane.mjs b/control-plane/proba-control-plane.mjs index 873e4bb..e39b05e 100644 --- a/control-plane/proba-control-plane.mjs +++ b/control-plane/proba-control-plane.mjs @@ -25,6 +25,9 @@ try { // fixtura vulnerabila 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"); + // 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 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"); @@ -34,9 +37,11 @@ try { const pv = ruleaza(v); 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: 2 actiuni in plan', pv.summary.total === 2); - const ecdh = pv.actions.find((a) => /ECDH/i.test(a.name || a.asset)); + cer('dosar vulnerabil: 3 folosiri vulnerabile (2 in a.js, 1 in tests/)', pv.inventory.quantumVulnerable === 3); + cer('dosar vulnerabil: 3 actiuni in plan', pv.summary.total === 3); + 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)); // 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)); @@ -56,5 +61,5 @@ try { } finally { 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); diff --git a/control-plane/proba-plan-migrare.mjs b/control-plane/proba-plan-migrare.mjs index c665039..7604805 100644 --- a/control-plane/proba-plan-migrare.mjs +++ b/control-plane/proba-plan-migrare.mjs @@ -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: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' }, + // 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, // 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']; 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 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']); diff --git a/control-plane/proba-raport-conformitate.mjs b/control-plane/proba-raport-conformitate.mjs index 2bbb895..bf8aa0f 100644 --- a/control-plane/proba-raport-conformitate.mjs +++ b/control-plane/proba-raport-conformitate.mjs @@ -32,6 +32,9 @@ try { '-days', '800', '-subj', '/CN=api.intern'], { stdio: 'ignore' }); } 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"); + // 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 scanerSrc = fs.readFileSync(caleaScanerului(), 'utf8'); const re = /add\('([a-z0-9-]+)',\s*'([a-z]+)',\s*[`']([^`']+)[`']/g; const reale = []; let m; @@ -50,6 +53,12 @@ try { // ---- raportul const { report: raport, envelope: plic, evidenceHash } = raportConformitate({ plan, organization: 'Exemplu SRL', date: '2026-09-28' }); 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:')); 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') diff --git a/control-plane/raport-conformitate.mjs b/control-plane/raport-conformitate.mjs index 63ff218..043f385 100644 --- a/control-plane/raport-conformitate.mjs +++ b/control-plane/raport-conformitate.mjs @@ -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, // 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 - 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); @@ -128,6 +131,7 @@ export function raportConformitate({ plan, organization, risk, date, cnsa = fals configuration: constatari.filter((c) => c.nature === 'configuration').length, unclassified: constatari.filter((c) => c.nature === 'unclassified').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, 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', @@ -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` : vulnerabile === 0 ? '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 = { v: 2, version: VERSIUNE, organization, date, risk: risk || null, cnsa: !!cnsa, regimes: Object.fromEntries(Object.entries(REGIMURI).filter(([k]) => cnsa || k !== 'cnsa-2.0')), diff --git a/control-plane/remediere.mjs b/control-plane/remediere.mjs index 7330989..4e05426 100644 --- a/control-plane/remediere.mjs +++ b/control-plane/remediere.mjs @@ -28,10 +28,10 @@ const HSTS = 'max-age=31536000'; // ce masuratoare sustine fiecare profil (un mesaj nu afirma mai mult decat masuratoarea) 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)', - 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', 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 -------------------------------------------------------------------- diff --git a/crypto-inventory/README.md b/crypto-inventory/README.md index 7dc50e6..2eea80e 100644 --- a/crypto-inventory/README.md +++ b/crypto-inventory/README.md @@ -19,11 +19,11 @@ An API name that appears only inside a comment or inside a string literal is not | 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` | -| 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`) | -| 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::`, `Hkdf::`, `type HmacSha256 = Hmac`, `SigningKey::`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::`, `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`, `cbc::Decryptor`); `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::`, `Hkdf::`, `type HmacSha256 = Hmac`, `SigningKey::`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::`, `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`, `cbc::Decryptor`); `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::::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 | | 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 ``` -node test/proba.mjs # 37 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/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 (25 mutations), and # 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/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: | `), and a probe fails on 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 diff --git a/crypto-inventory/lib/catalog.mjs b/crypto-inventory/lib/catalog.mjs index 508a251..997f805 100644 --- a/crypto-inventory/lib/catalog.mjs +++ b/crypto-inventory/lib/catalog.mjs @@ -289,7 +289,10 @@ export function evalueaza(a) { } if (g === 'HYBRID-KEX') { 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 === 'JWT-HMAC') { diff --git a/crypto-inventory/lib/detect-java.mjs b/crypto-inventory/lib/detect-java.mjs index 1f0bf23..1b52fbb 100644 --- a/crypto-inventory/lib/detect-java.mjs +++ b/crypto-inventory/lib/detect-java.mjs @@ -237,9 +237,44 @@ export function detecteazaJava(ctx) { const act = bcParam(m[1], m[2]); if (act) ctx.adauga(m.index, `${m[1]}Parameters.${m[2]}`, 'call', act, { bucata: m[0] }); } + detecteazaLibsignalJava(ctx); 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(/(? /^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) { let m; if (fam === 'MLKEM' && (m = /(512|768|1024)/.exec(p))) return { grup: 'MLKEM', param: m[1] }; diff --git a/crypto-inventory/lib/detect-js.mjs b/crypto-inventory/lib/detect-js.mjs index 6e4bfa8..f5ac11c 100644 --- a/crypto-inventory/lib/detect-js.mjs +++ b/crypto-inventory/lib/detect-js.mjs @@ -1,5 +1,5 @@ // 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'; 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}` }); } + // @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(/(? 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(`(?; type Kdf = Hkdf; - for (const m of ctx.potriviri(/(?;]+)>/g)) { + for (const m of ctx.potriviri(/(?;]+)>/g)) { const r = rezolvaCale(m[1], m[2]); if (!r) continue; 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; + // (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) { if (!/^[a-z_]\w*$/.test(local) || !CRATE[u.crate] || u.cale.length < 2) continue; - for (const m of ctx.potriviri(new RegExp(`(?)?\\s*\\(`, 'g'))) { + for (const m of ctx.potriviri(new RegExp(`(?)?\\s*\\(`, 'g'))) { 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 || '')); } @@ -306,17 +313,34 @@ export function detecteazaRust(ctx) { // 5. modurile de operare ale RustCrypto: cbc::Encryptor, ecb::Decryptor, ctr::Ctr128BE for (const m of ctx.potriviri(/(?`, '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::::new) + for (const m of ctx.potriviri(/(?`, '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 + // 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') || /(? u.crate === 'openssl') || /(? u.crate).find((c) => /^(openssl|boring|boring_signal)$/.test(c)) + || (/(? /^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('::')); - 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': { 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.' }; return null; 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 || ''); - if (!k || !/^(generate_key_pair|encapsulate|decapsulate|validate_public_key|validate_private_key)$/.test(fnume)) return null; - return { grup: 'MLKEM', param: k[1], functii: [/generate/.test(fnume) ? 'keygen' : /encaps/.test(fnume) ? 'encapsulate' : /decaps/.test(fnume) ? 'decapsulate' : 'other'] }; + return k ? { grup: 'MLKEM', param: k[1], functii: f } : null; } + 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': { 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; diff --git a/crypto-inventory/test/control-negativ.mjs b/crypto-inventory/test/control-negativ.mjs index 1e6d7cc..e7d5a9f 100644 --- a/crypto-inventory/test/control-negativ.mjs +++ b/crypto-inventory/test/control-negativ.mjs @@ -32,6 +32,38 @@ const MUTATII = [ tinta: 'rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey)', 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', fisier: 'lib/manifeste.mjs', diff --git a/crypto-inventory/test/echivalenta-0.1.0.mjs b/crypto-inventory/test/echivalenta-0.1.0.mjs index 331fc36..62c1da2 100644 --- a/crypto-inventory/test/echivalenta-0.1.0.mjs +++ b/crypto-inventory/test/echivalenta-0.1.0.mjs @@ -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)`); const ka = a.findings.map(norm), kb = b.findings.map(norm); const sa = new Set(ka), sb = new Set(kb); - const doarV = ka.filter((x) => !sb.has(x)), doarN = kb.filter((x) => !sa.has(x)); - const ok = !doarV.length && !doarN.length && ka.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries); + // 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, + // 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; 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}`); diff --git a/crypto-inventory/test/fixturi/java/Libsignal.java b/crypto-inventory/test/fixturi/java/Libsignal.java new file mode 100644 index 0000000..3f7b44a --- /dev/null +++ b/crypto-inventory/test/fixturi/java/Libsignal.java @@ -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 + } +} diff --git a/crypto-inventory/test/fixturi/java/LibsignalImport.java b/crypto-inventory/test/fixturi/java/LibsignalImport.java new file mode 100644 index 0000000..dd8d005 --- /dev/null +++ b/crypto-inventory/test/fixturi/java/LibsignalImport.java @@ -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); + } +} diff --git a/crypto-inventory/test/fixturi/js/libsignal-client-import.ts b/crypto-inventory/test/fixturi/js/libsignal-client-import.ts new file mode 100644 index 0000000..93aa5c0 --- /dev/null +++ b/crypto-inventory/test/fixturi/js/libsignal-client-import.ts @@ -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) }; +} diff --git a/crypto-inventory/test/fixturi/js/libsignal-client.ts b/crypto-inventory/test/fixturi/js/libsignal-client.ts new file mode 100644 index 0000000..4c4fca7 --- /dev/null +++ b/crypto-inventory/test/fixturi/js/libsignal-client.ts @@ -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 }; +} diff --git a/crypto-inventory/test/fixturi/negative/FaraLibsignal.java b/crypto-inventory/test/fixturi/negative/FaraLibsignal.java new file mode 100644 index 0000000..8bea36d --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/FaraLibsignal.java @@ -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); + } +} diff --git a/crypto-inventory/test/fixturi/negative/fara-libsignal.ts b/crypto-inventory/test/fixturi/negative/fara-libsignal.ts new file mode 100644 index 0000000..5d58c66 --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/fara-libsignal.ts @@ -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) }; +} diff --git a/crypto-inventory/test/fixturi/rust/libsignal.rs b/crypto-inventory/test/fixturi/rust/libsignal.rs new file mode 100644 index 0000000..b36b82d --- /dev/null +++ b/crypto-inventory/test/fixturi/rust/libsignal.rs @@ -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::::new_from_slice(key).unwrap(); // EXPECT: HMAC-SHA-256 | quantum-safe + let dec = Decryptor::::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())); +} diff --git a/crypto-inventory/test/proba.mjs b/crypto-inventory/test/proba.mjs index cd29c08..80b231a 100644 --- a/crypto-inventory/test/proba.mjs +++ b/crypto-inventory/test/proba.mjs @@ -18,8 +18,8 @@ const lx = await import(pathToFileURL(join(dirname(CALE_MODUL), 'lib', 'lexer.mj const CA_JSON = process.argv.includes('--json'); // numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient -const MARCAJE_ASTEPTATE = 128; -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 MARCAJE_ASTEPTATE = 158; +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 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', () => { 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'); 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', () => { comparaExact('negative/variabile.js'); const g = gasiriDin(scan(FIX), 'negative/variabile.js'); diff --git a/pq-gateway/Dockerfile b/pq-gateway/Dockerfile index 6c62329..98fa34e 100644 --- a/pq-gateway/Dockerfile +++ b/pq-gateway/Dockerfile @@ -1,5 +1,6 @@ # 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. # # docker build -t aere-pq-gateway . diff --git a/pq-gateway/README.md b/pq-gateway/README.md index 47e8eaa..1915369 100644 --- a/pq-gateway/README.md +++ b/pq-gateway/README.md @@ -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 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 -carries (the test suite runs the gateway directly on Node.js 24 with OpenSSL 3.5.5). +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`): +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 diff --git a/pq-gateway/dovezi/imagine-docker-2026-10-18.md b/pq-gateway/dovezi/imagine-docker-2026-10-18.md new file mode 100644 index 0000000..a4b2215 --- /dev/null +++ b/pq-gateway/dovezi/imagine-docker-2026-10-18.md @@ -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. diff --git a/pq-gateway/dovezi/pqgw-imagine-rpc2.sh b/pq-gateway/dovezi/pqgw-imagine-rpc2.sh new file mode 100644 index 0000000..1b0cea0 --- /dev/null +++ b/pq-gateway/dovezi/pqgw-imagine-rpc2.sh @@ -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)" diff --git a/pq-gateway/dovezi/pqgw-sonda.mjs b/pq-gateway/dovezi/pqgw-sonda.mjs new file mode 100644 index 0000000..b5ae569 --- /dev/null +++ b/pq-gateway/dovezi/pqgw-sonda.mjs @@ -0,0 +1,14 @@ +// pqgw-sonda.mjs : 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; });