crypto-inventory 0.3.0 and the control-plane planner after the second run on a tree that is not ours (libsignal): BoringSSL, Noise hfs, libcrux Kyber, the libsignal Java/Node API; self-signed trust anchors are blocked, test code is LOW with its own scope
This commit is contained in:
parent
42b049ef04
commit
e453c0c42d
@ -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=<verify-proof.mjs from aere.network/tools> node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) |
|
||||
| proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test |
|
||||
| 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 <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).
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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 <dir> [--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 : ''}`);
|
||||
|
||||
22
control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md
Normal file
22
control-plane/dovezi/retete-2026-10-18/CITESTE-MA.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Retetele de remediere `go` si `nginx`, masurate cap la cap, 2026-10-18 04:28Z-04:31Z
|
||||
|
||||
Pana azi `remediere.mjs` spunea despre profilul `nginx` "the server directive from the nginx documentation, not measured by us on a
|
||||
real nginx" si despre `go` "from the Go 1.24 release notes; not measured by us (the Go on the test machine is 1.22)". Masurat azi, cu
|
||||
EXACT fragmentele pe care le scrie reteta pentru `hndl-exposed` + `tls12-accepted` + `hsts-missing` (setarile: grupurile
|
||||
`X25519MLKEM768:X25519`, versiunea minima 1.3, HSTS un an), fiecare server pornit pe 127.0.0.1 si sondat cu Node 24 (`tls-sonda.mjs`:
|
||||
o strangere de mana reala cu grupurile oferite si, optional, cu versiunea maxima) si cu `curl -k` pentru antet:
|
||||
|
||||
| | Go (`go/main.go`, laptop) | nginx (`masoara-nginx-rpc2.sh`, container pe rpc2) |
|
||||
|---|---|---|
|
||||
| serverul | Go **1.26.5** windows/amd64, `&tls.Config{MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}}`, `w.Header().Set("Strict-Transport-Security", "max-age=31536000")` | **nginx 1.31.7** din `nginx:alpine`, "built with OpenSSL 3.5.8 (running with OpenSSL 3.5.9)"; `ssl_ecdh_curve X25519MLKEM768:X25519; ssl_protocols TLSv1.3; add_header Strict-Transport-Security "max-age=31536000" always;`; `nginx -t` ok |
|
||||
| client care ofera numai `X25519MLKEM768` | reusit (TLSv1.3, TLS_AES_128_GCM_SHA256) | reusit (TLSv1.3, grup raportat `X25519MLKEM768`) |
|
||||
| client care ofera numai `X25519` (rezerva din reteta) | reusit, grup `X25519` | reusit, grup `X25519` |
|
||||
| client `X25519MLKEM768:X25519` | reusit | reusit, grupul ales `X25519MLKEM768` |
|
||||
| client TLS 1.2 | **refuzat** `ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION` | **refuzat** `ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION` |
|
||||
| HSTS | `Strict-Transport-Security: max-age=31536000` | `Strict-Transport-Security: max-age=31536000` |
|
||||
| scanerul AERE pe server (`scaneaza-local.mjs`, `scaneazaAdresa` pe 127.0.0.1) | `pqKeyExchange X25519MLKEM768, prefersPqWhenOffered true, tls12Accepted false, hsts true`; scor 70 numai din certificatul de proba (autosemnat, 2 zile: `cert-expiring`, `chain-untrusted`) | NEMASURAT cu scanerul (modulul nu e instalat pe rpc2 in forma importabila); sondele si curl-ul acopera aceleasi fapte |
|
||||
| curatenie | serverul oprit, binarul sters | container si imagine sterse; RPC-ul local 11 -> 13 ms |
|
||||
|
||||
Ce NU s-a masurat: Apache si HAProxy (nimic pe nicio gazda a noastra; raman "din documentatie"), un nginx de productie cu configuratia
|
||||
lui intreaga (aici un singur `server {}` de proba), Go sub Windows numai (Linux NEMASURAT; Go-ul din WSL e 1.22, sub 1.24), clienti
|
||||
reali (browsere) spre aceste servere. Jurnalele: `masoara-go.log`, `masoara-nginx.log`.
|
||||
48
control-plane/dovezi/retete-2026-10-18/go/main.go
Normal file
48
control-plane/dovezi/retete-2026-10-18/go/main.go
Normal file
@ -0,0 +1,48 @@
|
||||
// Serverul de proba al retetei Go din remediere.mjs (profilul `go`, pentru hndl-exposed + tls12-accepted + hsts-missing):
|
||||
// &tls.Config{MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}}
|
||||
// w.Header().Set("Strict-Transport-Security", "max-age=31536000")
|
||||
// Certificat autosemnat generat la pornire (ECDSA P-256), asculta pe 127.0.0.1:<port>, raspunde "ok" la orice cale.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := "8445"
|
||||
if len(os.Args) > 1 {
|
||||
port = os.Args[1]
|
||||
}
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tmpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "proba.local"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(48 * time.Hour), DNSNames: []string{"proba.local", "localhost"}}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cert := tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
|
||||
// EXACT fragmentul retetei (remediere.mjs, profilul go, setarile groups + tlsMin 1.3 + hsts):
|
||||
cfg := &tls.Config{MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.X25519}, Certificates: []tls.Certificate{cert}}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=31536000")
|
||||
fmt.Fprintln(w, "ok")
|
||||
})
|
||||
srv := &http.Server{Addr: "127.0.0.1:" + port, Handler: mux, TLSConfig: cfg}
|
||||
fmt.Printf("listening 127.0.0.1:%s go=%s\n", port, runtime.Version())
|
||||
log.Fatal(srv.ListenAndServeTLS("", ""))
|
||||
}
|
||||
30
control-plane/dovezi/retete-2026-10-18/masoara-go.sh
Normal file
30
control-plane/dovezi/retete-2026-10-18/masoara-go.sh
Normal file
@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# masoara-go.sh: reteta Go a remedierii (profilul `go`) masurata cap la cap pe laptop: serverul din main.go (EXACT fragmentul retetei),
|
||||
# construit cu Go-ul de pe Windows, pornit pe 127.0.0.1:8445; sonde Node 24 (OpenSSL 3.5.x): hibrid singur (trebuie ok), X25519 singur
|
||||
# (reteta il lasa ca rezerva: trebuie ok, grup X25519), client TLS 1.2 (MinVersion 1.3: trebuie refuzat), HSTS prin curl -k, si scanerul
|
||||
# AERE pe server (ce ar vedea clientul dupa reteta). Serverul e oprit la sfarsit; codul final e 0 numai cu toate cum trebuie.
|
||||
set -u
|
||||
D="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$D/go" || exit 2
|
||||
echo "== go"; go version
|
||||
go build -o proba-go-tls.exe main.go || { echo "build cazut"; exit 1; }
|
||||
./proba-go-tls.exe 8445 > "$D/go-server.log" 2>&1 &
|
||||
PID=$!
|
||||
sleep 2
|
||||
echo "server: $(head -1 "$D/go-server.log")"
|
||||
rele=0
|
||||
cere() { # $1 nume, $2 cod asteptat, $3... comanda
|
||||
local nume="$1" ast="$2"; shift 2
|
||||
out=$("$@" 2>&1); c=$?
|
||||
if [ "$c" = "$ast" ]; then echo " OK $nume -> cod $c: $(printf '%s' "$out" | cut -c1-170)"; else echo " RAU $nume -> cod $c (asteptat $ast): $(printf '%s' "$out" | cut -c1-170)"; rele=$((rele + 1)); fi
|
||||
}
|
||||
cere "hibrid singur (X25519MLKEM768)" 0 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519MLKEM768
|
||||
cere "clasic singur (X25519, rezerva din reteta)" 0 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519
|
||||
cere "hibrid + clasic (preferinta)" 0 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519MLKEM768:X25519
|
||||
cere "client TLS 1.2 (MinVersion 1.3: refuz)" 1 node "$D/tls-sonda.mjs" 127.0.0.1 8445 X25519 TLSv1.2
|
||||
echo "-- HSTS (curl -k, antetul)"; curl -sk -m 10 -D - -o /dev/null https://127.0.0.1:8445/ | grep -iE '^(strict-transport-security|HTTP)' | tr -d '\r'
|
||||
echo "-- scanerul AERE pe serverul retetei"; node "$D/scaneaza-local.mjs" 8445 | cut -c1-700
|
||||
kill $PID 2>/dev/null; sleep 1; ps -p $PID > /dev/null 2>&1 && { echo "serverul nu s-a oprit; taskkill"; taskkill //F //PID $PID > /dev/null 2>&1; }
|
||||
rm -f proba-go-tls.exe
|
||||
echo "reteta go: $((4 - rele)) din 4 sonde cum trebuie"
|
||||
[ "$rele" = 0 ] && exit 0 || exit 1
|
||||
44
control-plane/dovezi/retete-2026-10-18/masoara-nginx-rpc2.sh
Normal file
44
control-plane/dovezi/retete-2026-10-18/masoara-nginx-rpc2.sh
Normal file
@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# masoara-nginx-rpc2.sh (PE rpc2, trimis cu `ssh ... 'bash -s'`): reteta nginx a remedierii (profilul `nginx`) masurata pe un nginx REAL,
|
||||
# in container (nginx:alpine, cu OpenSSL-ul lui Alpine), pe 127.0.0.1:8446, cu EXACT directivele retetei pentru hndl-exposed +
|
||||
# tls12-accepted + hsts-missing: `ssl_ecdh_curve X25519MLKEM768:X25519; ssl_protocols TLSv1.3; add_header Strict-Transport-Security
|
||||
# "max-age=31536000" always;`. Preconditia retetei (nginx legat de OpenSSL >= 3.5) se citeste din `nginx -V`. Sonde: node 24 de pe rpc2
|
||||
# (hibrid singur, clasic singur, TLS 1.2 refuzat), HSTS prin curl. Container si imagine sterse la sfarsit. RPC-ul local cronometrat.
|
||||
set -u
|
||||
D=/root/nginx-reteta; rm -rf "$D"; mkdir -p "$D/certs" "$D/conf"; cd "$D" || exit 2
|
||||
N=/opt/node24/bin/node
|
||||
rpc() { local t0=$(date +%s%N); curl -s -m 5 -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:8545 | grep -o '"result":"0x[0-9a-f]*"' | head -1; echo " ($(( ($(date +%s%N) - t0) / 1000000 )) ms)"; }
|
||||
echo "== RPC inainte: $(rpc)"
|
||||
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout certs/privkey.pem -out certs/fullchain.pem -days 2 -subj '/CN=proba.local' > /dev/null 2>&1 && chmod 644 certs/*.pem && echo "cert ok"
|
||||
cat > conf/default.conf <<'EOF'
|
||||
server {
|
||||
listen 127.0.0.1:8446 ssl;
|
||||
server_name proba.local;
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
# EXACT fragmentul retetei (remediere.mjs, profilul nginx):
|
||||
ssl_ecdh_curve X25519MLKEM768:X25519;
|
||||
ssl_protocols TLSv1.3;
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
location / { return 200 "ok\n"; }
|
||||
}
|
||||
EOF
|
||||
cp tls-sonda.mjs "$D/" 2>/dev/null || true
|
||||
echo "== imaginea nginx:alpine"; docker pull -q nginx:alpine > /dev/null 2>&1; docker image inspect nginx:alpine --format 'imagine: {{.Id}} {{.Size}} octeti' | cut -c1-90
|
||||
echo "-- preconditia retetei: nginx -V (OpenSSL)"; docker run --rm nginx:alpine nginx -V 2>&1 | grep -iE 'nginx version|openssl' | cut -c1-160
|
||||
docker rm -f nginx-reteta > /dev/null 2>&1
|
||||
docker run -d --name nginx-reteta --network host -v "$D/conf/default.conf:/etc/nginx/conf.d/default.conf:ro" -v "$D/certs:/etc/nginx/certs:ro" nginx:alpine > /dev/null; sleep 2
|
||||
docker ps --filter name=nginx-reteta --format 'container: {{.Status}}'
|
||||
echo "-- nginx -t in container"; docker exec nginx-reteta nginx -t 2>&1 | tail -1
|
||||
rele=0
|
||||
cere() { local nume="$1" ast="$2"; shift 2; out=$("$@" 2>&1); c=$?; if [ "$c" = "$ast" ]; then echo " OK $nume -> cod $c: $(printf '%s' "$out" | cut -c1-170)"; else echo " RAU $nume -> cod $c (asteptat $ast): $(printf '%s' "$out" | cut -c1-170)"; rele=$((rele + 1)); fi; }
|
||||
cere "hibrid singur (X25519MLKEM768)" 0 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519MLKEM768
|
||||
cere "clasic singur (X25519, rezerva din reteta)" 0 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519
|
||||
cere "hibrid + clasic (preferinta)" 0 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519MLKEM768:X25519
|
||||
cere "client TLS 1.2 (ssl_protocols TLSv1.3: refuz)" 1 $N /root/nginx-reteta-sonda.mjs 127.0.0.1 8446 X25519 TLSv1.2
|
||||
echo "-- HSTS (curl -k)"; curl -sk -m 10 -D - -o /dev/null https://127.0.0.1:8446/ | grep -iE '^(strict-transport-security|HTTP)' | tr -d '\r'
|
||||
docker logs nginx-reteta 2>&1 | grep -iE 'emerg|error' | head -3
|
||||
echo "== curatenie"; docker rm -f nginx-reteta > /dev/null 2>&1; docker rmi nginx:alpine > /dev/null 2>&1 && echo "imaginea stearsa"; cd /root && rm -rf "$D" /root/nginx-reteta-sonda.mjs; docker ps -a --format '{{.Names}}' | grep -c nginx-reteta | sed 's/^/containere ramase: /'
|
||||
echo "== RPC dupa: $(rpc)"
|
||||
echo "reteta nginx: $((4 - rele)) din 4 sonde cum trebuie"
|
||||
[ "$rele" = 0 ] && exit 0 || exit 1
|
||||
@ -0,0 +1,8 @@
|
||||
// scaneaza-local.mjs <port>: scanerul AERE de pregatire post-cuantica (cloud-gateway/readiness-service.mjs, functia scaneazaAdresa,
|
||||
// fara garda de adrese private: tinta e 127.0.0.1, aleasa de noi) pe serverul de proba al unei retete. Tipareste verdictul, rezumatul si
|
||||
// constatarile, exact ce ar vedea un client al scanerului pe serverul lui dupa aplicarea retetei.
|
||||
import { pathToFileURL } from 'node:url';
|
||||
const port = Number(process.argv[2] || 8445);
|
||||
const { scaneazaAdresa } = await import(pathToFileURL('C:/Users/Jessica/Documents/AereNetwork/aerenew/cloud-gateway/readiness-service.mjs').href);
|
||||
const r = await scaneazaAdresa('proba.local', { address: '127.0.0.1', family: 4 }, { port, jurnal: false });
|
||||
console.log(JSON.stringify({ score: r.score, verdict: r.verdict, summary: r.summary, findings: (r.findings || []).map((f) => f.id + ':' + f.severity), error: r.error, detail: r.detail }, null, 1).replace(/\n\s*/g, ' '));
|
||||
15
control-plane/dovezi/retete-2026-10-18/tls-sonda.mjs
Normal file
15
control-plane/dovezi/retete-2026-10-18/tls-sonda.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
// tls-sonda.mjs <gazda> <port> <grupuri> [maxVersion]: o strangere de mana TLS reala cu grupurile de schimb de cheie date (de ex.
|
||||
// X25519MLKEM768 singur, X25519 singur) si, optional, cu versiunea maxima (TLSv1.2 = un client care nu stie TLS 1.3). Fara verificarea
|
||||
// certificatului (al probei e autosemnat). JSON pe stdout; iesire 0 = handshake reusit, 1 = refuzat.
|
||||
import tls from 'node:tls';
|
||||
const [gazda, port, grupuri, maxV] = process.argv.slice(2);
|
||||
const t0 = Date.now();
|
||||
const opt = { host: gazda, port: Number(port), servername: 'proba.local', rejectUnauthorized: false, ecdhCurve: grupuri, ALPNProtocols: ['http/1.1'] };
|
||||
if (maxV) { opt.maxVersion = maxV; opt.minVersion = maxV; } else { opt.minVersion = 'TLSv1.3'; opt.maxVersion = 'TLSv1.3'; }
|
||||
const s = tls.connect(opt, () => {
|
||||
const ek = s.getEphemeralKeyInfo();
|
||||
console.log(JSON.stringify({ ok: true, grupuriOferite: grupuri, versiuneCeruta: maxV || 'TLSv1.3', protocol: s.getProtocol(), cifru: s.getCipher().name, grupRaportat: ek && ek.name, ms: Date.now() - t0, openssl: process.versions.openssl }));
|
||||
s.end(); process.exitCode = 0;
|
||||
});
|
||||
s.setTimeout(8000, () => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, versiuneCeruta: maxV || 'TLSv1.3', motiv: 'timeout' })); s.destroy(); process.exitCode = 1; });
|
||||
s.on('error', (e) => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, versiuneCeruta: maxV || 'TLSv1.3', motiv: e.code || e.message, ms: Date.now() - t0, openssl: process.versions.openssl })); process.exitCode = 1; });
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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']);
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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')),
|
||||
|
||||
@ -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 --------------------------------------------------------------------
|
||||
|
||||
@ -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::<Sha256>`, `Hkdf::<Sha512>`, `type HmacSha256 = Hmac<Sha256>`, `SigningKey::<Sha2_128s>`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::<Sha256>`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor<Aes128>`, `cbc::Decryptor<Aes256>`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr`; libcrux (`libcrux_ml_kem::mlkem1024::generate_key_pair`, `libcrux_ml_dsa::ml_dsa_65::sign`); a name brought in by a glob import (`use libsignal_protocol::*`) is resolved by trying the glob-imported crates in order |
|
||||
| Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::<Sha256>`, `Hkdf::<Sha512>`, `type HmacSha256 = Hmac<Sha256>`, `SigningKey::<Sha2_128s>`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::<Sha256>`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor<Aes128>`, `cbc::Decryptor<Aes256>`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` and the BoringSSL bindings `boring` / `boring-signal` with the same API (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `SslConnector::builder`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr`; libcrux (`libcrux_ml_kem::mlkem1024::generate_key_pair`, the pre-standard `kyber768` / `kyber1024` modules, `libcrux_ml_dsa::ml_dsa_65::sign`); `x25519_dalek::x25519(...)`; `HmacReset`; a mode type brought in by name (`use cbc::Decryptor; Decryptor::<Aes256>::new`); the hybrid Noise pattern `Noise_NKhfs_25519+Kyber1024_...` as one hybrid key exchange; a name brought in by a glob import (`use libsignal_protocol::*`) is resolved by trying the glob-imported crates in order |
|
||||
| Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys |
|
||||
| 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: <name> | <class>`), 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
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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(/(?<![\w.])(?:import\s+(?:static\s+)?org\.signal\.libsignal\.(?:protocol|crypto|metadata|zkgroup|svr|keytrans)\b|package\s+org\.signal\.libsignal\b)/g);
|
||||
if (!ancore.length) return;
|
||||
for (const m of ctx.potriviri(/(?<![\w.])(?:(ECKeyPair|IdentityKeyPair|ECPrivateKey)\s*\.\s*generate|(Curve)\s*\.\s*generateKeyPair)\s*\(/g)) {
|
||||
ctx.adauga(m.index, `${m[1] || m[2]}.${m[1] ? 'generate' : 'generateKeyPair'}`, 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: LIBSIGNAL_NOTA_EC }, { bucata: m[0] });
|
||||
}
|
||||
for (const m of ctx.potriviri(/(?<![\w.])KEMKeyPair\s*\.\s*generate\s*\(\s*(?:KEMKeyType\s*\.\s*([A-Za-z_0-9]+))?/g)) {
|
||||
ctx.adauga(m.index, 'KEMKeyPair.generate', 'call', kemLibsignal(m[1]), { bucata: m[0].trim() });
|
||||
}
|
||||
for (const m of ctx.potriviri(/(?<![\w.])HKDF\s*\.\s*deriveSecrets\s*\(/g)) {
|
||||
ctx.adauga(m.index, 'HKDF.deriveSecrets', 'call', { grup: 'KDF', nume: 'HKDF', hash: 'SHA256', functii: ['keyderive'], nota: 'libsignal HKDF is HKDF-SHA-256.' }, { bucata: m[0] });
|
||||
}
|
||||
for (const m of ctx.potriviri(/(?<![\w.])new\s+(Aes256GcmSiv|Aes256GcmEncryption|Aes256GcmDecryption|Aes256Ctr32)\s*\(/g)) {
|
||||
const mod = m[1] === 'Aes256GcmSiv' ? 'gcm-siv' : m[1] === 'Aes256Ctr32' ? 'ctr' : 'gcm';
|
||||
ctx.adauga(m.index, `new ${m[1]}`, 'call', { grup: 'CIPHER', nume: 'AES', param: '256', mod, functii: [m[1] === 'Aes256GcmDecryption' ? 'decrypt' : 'encrypt'] }, { bucata: m[0] });
|
||||
}
|
||||
for (const m of ctx.potriviri(/(?<![\w.])new\s+PreKeyBundle\s*\(/g)) ctx.adauga(m.index, 'new PreKeyBundle', 'call', LIBSIGNAL_BUNDLE, { bucata: m[0] });
|
||||
// importul: dovada slaba, o singura data pe fisier, suprimata de orice apel recunoscut de mai sus
|
||||
const imp = ancore.find((a) => /^import/.test(a[0]));
|
||||
if (imp) ctx.adauga(imp.index, 'import org.signal.libsignal', 'import', { grup: 'LIB-MULTI', nume: 'libsignal (org.signal.libsignal)', motiv: 'Imports the Signal protocol library (Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet, SPQR, sealed sender); no specific call recognized in this file.' }, { suprimaDe: ['XDH', 'PREPQ', 'MLKEM', 'KDF', 'CIPHER', 'LIB-MULTI'], bucata: imp[0].trim() });
|
||||
}
|
||||
|
||||
function bcParam(fam, p) {
|
||||
let m;
|
||||
if (fam === 'MLKEM' && (m = /(512|768|1024)/.exec(p))) return { grup: 'MLKEM', param: m[1] };
|
||||
|
||||
@ -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(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?(PrivateKey|IdentityKeyPair)\s*\.\s*generate\s*\(/g)) {
|
||||
ctx.adauga(m.index, `${m[1]}.generate`, 'call', { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: notaEc }, { bucata: m[0] });
|
||||
}
|
||||
for (const m of ctx.potriviri(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?KEMKeyPair\s*\.\s*generate\s*\(/g)) {
|
||||
ctx.adauga(m.index, 'KEMKeyPair.generate', 'call', { grup: 'PREPQ', nume: 'Kyber-1024', primitiv: 'kem', functii: ['keygen'], nota: 'libsignal-client KEMKeyPair.generate() produces a Kyber-1024 pair (the PQXDH prekey; the only KEM type the Node API exposed when this rule was written).' }, { bucata: m[0] });
|
||||
}
|
||||
// hkdf: numai numele importat din pachet sau prin aliasul lui de spatiu de nume (node:crypto are si el un hkdf, recunoscut mai sus)
|
||||
const numeHkdf = lsc.flatMap((i) => i.locale.filter((_, k) => i.importate[k] === 'hkdf'));
|
||||
const aliasuri = lsc.map((i) => i.implicit).filter(Boolean);
|
||||
const forme = [...numeHkdf.map((n) => `${escapeRe(n)}`), ...aliasuri.map((a) => `${escapeRe(a)}\\s*\\.\\s*hkdf`)];
|
||||
if (forme.length) {
|
||||
for (const m of ctx.potriviri(new RegExp(`(?<![\\w$.])(?:${forme.join('|')})\\s*\\(`, 'g'))) {
|
||||
ctx.adauga(m.index, 'hkdf', 'call', { grup: 'KDF', nume: 'HKDF', hash: 'SHA256', functii: ['keyderive'], nota: 'libsignal HKDF is HKDF-SHA-256.' }, { bucata: m[0] });
|
||||
}
|
||||
}
|
||||
for (const m of ctx.potriviri(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?Aes256GcmSiv\s*\.\s*new\s*\(/g)) {
|
||||
ctx.adauga(m.index, 'Aes256GcmSiv.new', 'call', { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'gcm-siv', functii: ['encrypt'] }, { bucata: m[0] });
|
||||
}
|
||||
for (const m of ctx.potriviri(/(?<![\w$.])(?:[\w$]+\s*\.\s*)?(PreKeyBundle\s*\.\s*new|processPreKeyBundle)\s*\(/g)) {
|
||||
ctx.adauga(m.index, m[1].replace(/\s+/g, ''), 'call', bundle, { bucata: m[0] });
|
||||
}
|
||||
ctx.adauga(lsc[0].pos, `import ${lsc[0].spec}`, 'import', { grup: 'LIB-MULTI', nume: '@signalapp/libsignal-client', motiv: 'Imports the Signal protocol library for Node (Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet, SPQR, sealed sender); no specific call recognized in this file.' }, { suprimaDe: ['XDH', 'PREPQ', 'MLKEM', 'KDF', 'CIPHER', 'LIB-MULTI'], bucata: `import ${lsc[0].spec}` });
|
||||
}
|
||||
|
||||
// node-rsa
|
||||
const nrsa = importa(/^node-rsa$/);
|
||||
if (nrsa.length) {
|
||||
|
||||
@ -68,6 +68,11 @@ const CRATE = {
|
||||
libsignal_protocol: { ofera: 'Signal sessions: Curve25519 identity and prekeys, Kyber prekeys (PQXDH), Double Ratchet', grupuri: ['XDH', 'PREPQ', 'MLKEM', 'LIB-MULTI'] },
|
||||
ring: { ofera: 'ring: AES-GCM, ChaCha20-Poly1305, SHA-2, HMAC, HKDF, PBKDF2, Ed25519, ECDSA P-256/P-384, RSA, X25519, ECDH', grupuri: ['HASH', 'MAC', 'KDF', 'CIPHER', 'EDDSA', 'ECDSA', 'RSA', 'XDH', 'ECDH'] },
|
||||
openssl: { ofera: 'OpenSSL bindings', grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] },
|
||||
// 2026-10-18 (libsignal): BoringSSL bindings, acelasi API ca `openssl`; `boring-signal` e forkul lui Signal, stiva TLS a lui libsignal
|
||||
boring: { ofera: 'BoringSSL bindings (same API as openssl)', grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] },
|
||||
boring_signal: { ofera: "BoringSSL bindings (Signal's fork of boring; the TLS stack under libsignal)", grupuri: ['HASH', 'CIPHER', 'RSA', 'EC', 'ECDSA', 'EDDSA', 'XDH', 'TLS'] },
|
||||
tokio_boring: { ofera: 'TLS over tokio (BoringSSL)', grupuri: ['TLS'] },
|
||||
tokio_boring_signal: { ofera: 'TLS over tokio (BoringSSL, Signal fork)', grupuri: ['TLS'] },
|
||||
rustls: { ofera: 'TLS (versions and key-exchange groups come from the crypto provider and the Cargo features)', grupuri: ['TLS', 'XDH', 'UNKNOWN'] },
|
||||
tokio_rustls: { ofera: 'TLS over tokio (rustls)', grupuri: ['TLS'] },
|
||||
hyper_rustls: { ofera: 'HTTPS for hyper (rustls)', grupuri: ['TLS'] },
|
||||
@ -87,7 +92,7 @@ for (const [crate, tipuri] of Object.entries({
|
||||
sha1: ['Sha1'], md5: ['Md5'],
|
||||
sha3: ['Sha3_224', 'Sha3_256', 'Sha3_384', 'Sha3_512', 'Keccak256', 'Shake128', 'Shake256'],
|
||||
blake2: ['Blake2b512', 'Blake2s256', 'Blake2b', 'Blake2s', 'Blake2bMac', 'Blake2sMac'],
|
||||
hmac: ['Hmac', 'SimpleHmac'], hkdf: ['Hkdf'], argon2: ['Argon2'],
|
||||
hmac: ['Hmac', 'SimpleHmac', 'HmacReset', 'SimpleHmacReset'], hkdf: ['Hkdf'], argon2: ['Argon2'],
|
||||
chacha20poly1305: ['ChaCha20Poly1305', 'XChaCha20Poly1305', 'ChaCha8Poly1305', 'ChaCha12Poly1305', 'XChaCha8Poly1305', 'XChaCha12Poly1305'],
|
||||
aes_gcm: ['Aes128Gcm', 'Aes256Gcm'], aes_gcm_siv: ['Aes128GcmSiv', 'Aes256GcmSiv'], aes_siv: ['Aes128SivAead', 'Aes256SivAead'],
|
||||
aes: ['Aes128', 'Aes192', 'Aes256'],
|
||||
@ -233,7 +238,7 @@ export function detecteazaRust(ctx) {
|
||||
}
|
||||
|
||||
// 2. aliasuri de tip cu parametru generic: type HmacSha256 = Hmac<Sha256>; type Kdf = Hkdf<Sha512>;
|
||||
for (const m of ctx.potriviri(/(?<![\w:])type\s+\w+\s*=\s*((?:[A-Za-z_]\w*::)*)(Hmac|SimpleHmac|Hkdf)\s*<\s*([^>;]+)>/g)) {
|
||||
for (const m of ctx.potriviri(/(?<![\w:])type\s+\w+\s*=\s*((?:[A-Za-z_]\w*::)*)(Hmac|SimpleHmac|HmacReset|SimpleHmacReset|Hkdf)\s*<\s*([^>;]+)>/g)) {
|
||||
const r = rezolvaCale(m[1], m[2]);
|
||||
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(`(?<![\\w:.])${escapeRe(local)}\\s*(?:::\\s*<([^;{}()=]*?)>)?\\s*\\(`, 'g'))) {
|
||||
for (const m of ctx.potriviri(new RegExp(`(?<![\\w:.])(?<!\\bfn\\s+)${escapeRe(local)}\\s*(?:::\\s*<([^;{}()=]*?)>)?\\s*\\(`, 'g'))) {
|
||||
const activ = activFunctie(u.cale, m[1], ctx, m);
|
||||
if (activ) adauga(u.crate, m.index, u.cale.join('::'), 'call', activ, m[0].trim() + (activ.bucataExtra || ''));
|
||||
}
|
||||
@ -306,17 +313,34 @@ export function detecteazaRust(ctx) {
|
||||
// 5. modurile de operare ale RustCrypto: cbc::Encryptor<Aes256>, ecb::Decryptor<aes::Aes128>, ctr::Ctr128BE<Aes256>
|
||||
for (const m of ctx.potriviri(/(?<![\w:])(cbc|ecb|ctr|cfb_mode|ofb|ccm)::(Encryptor|Decryptor|Ctr\w*|Ccm)\s*(?:::)?\s*<\s*(?:aes::)?Aes(128|192|256)\b/g)) {
|
||||
const mod = m[1] === 'cfb_mode' ? 'cfb' : m[1];
|
||||
folosite.add(m[1]); // crate-ul modului e folosit, nu doar aes (altfel `use cbc` iesea 'import fara apel' langa propriul lui apel)
|
||||
adauga('aes', m.index, `${m[1]}::${m[2]}<Aes${m[3]}>`, 'call', { grup: 'CIPHER', nume: 'AES', param: m[3], mod, functii: [m[2] === 'Decryptor' ? 'decrypt' : 'encrypt'] }, m[0]);
|
||||
}
|
||||
// 2026-10-18 (libsignal): acelasi tip de mod adus prin `use cbc::Decryptor;` si scris fara prefix (Decryptor::<Aes256>::new)
|
||||
for (const m of ctx.potriviri(/(?<![\w:])(Encryptor|Decryptor|Ctr\w*|Ccm)\s*(?:::)?\s*<\s*(?:aes::)?Aes(128|192|256)\b/g)) {
|
||||
const u = locale.get(m[1]);
|
||||
if (!u || !/^(cbc|ecb|ctr|cfb_mode|ofb|ccm)$/.test(u.crate)) continue;
|
||||
const mod = u.crate === 'cfb_mode' ? 'cfb' : u.crate;
|
||||
folosite.add(u.crate);
|
||||
adauga('aes', m.index, `${u.crate}::${m[1]}<Aes${m[2]}>`, 'call', { grup: 'CIPHER', nume: 'AES', param: m[2], mod, functii: [m[1] === 'Decryptor' ? 'decrypt' : 'encrypt'] }, m[0]);
|
||||
}
|
||||
|
||||
// 6. tiparul Noise al lui snow, scris ca sir: Noise_XX_25519_ChaChaPoly_SHA256
|
||||
// 2026-10-18 (libsignal): si forma hibrida hfs, Noise_NKhfs_25519+Kyber1024_ChaChaPoly_SHA256: X25519 si KEM-ul in aceeasi strangere
|
||||
// de mana (hybrid forward secrecy), deci un schimb de cheie HIBRID, nu un X25519 singur peste care sa se ceara ML-KEM
|
||||
if ([...locale.values()].some((u) => u.crate === 'snow') || /(?<![\w:])snow::/.test(ctx.code)) {
|
||||
for (const m of ctx.code.matchAll(/Noise_(\w+?)_(25519|448)_(ChaChaPoly|AESGCM)_(SHA256|SHA512|BLAKE2s|BLAKE2b)\b/g)) {
|
||||
for (const m of ctx.code.matchAll(/Noise_(\w+?)_(25519|448)(?:\+(Kyber|MLKEM|ML_KEM|mlkem)(512|768|1024))?_(ChaChaPoly|AESGCM)_(SHA256|SHA512|BLAKE2s|BLAKE2b)\b/g)) {
|
||||
if (!ctx.inSir(m.index)) continue;
|
||||
const pos = m.index;
|
||||
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'XDH', nume: m[2] === '448' ? 'X448' : 'X25519', functii: ['keygen'] }, m[0]);
|
||||
adauga('snow', pos, 'snow Noise pattern', 'config', m[3] === 'AESGCM' ? { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'gcm', functii: ['encrypt'] } : { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'] }, m[0]);
|
||||
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'HASH', hash: m[4] === 'SHA256' ? 'SHA256' : m[4] === 'SHA512' ? 'SHA512' : m[4] === 'BLAKE2s' ? 'BLAKE2S' : 'BLAKE2B', functii: ['digest'] }, m[0]);
|
||||
const dh = m[2] === '448' ? 'X448' : 'X25519';
|
||||
if (m[3]) {
|
||||
const kem = /kyber/i.test(m[3]) ? `Kyber${m[4]}` : `ML-KEM-${m[4]}`;
|
||||
adauga('snow', pos, 'snow Noise pattern (hfs)', 'config', { grup: 'HYBRID-KEX', nume: `${dh}+${kem} (Noise hfs)`, nota: `Noise hybrid forward secrecy: the ${dh} and the ${kem} shared secrets both enter the handshake key schedule.` }, m[0]);
|
||||
} else {
|
||||
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'XDH', nume: dh, functii: ['keygen'] }, m[0]);
|
||||
}
|
||||
adauga('snow', pos, 'snow Noise pattern', 'config', m[5] === 'AESGCM' ? { grup: 'CIPHER', nume: 'AES', param: '256', mod: 'gcm', functii: ['encrypt'] } : { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'] }, m[0]);
|
||||
adauga('snow', pos, 'snow Noise pattern', 'config', { grup: 'HASH', hash: m[6] === 'SHA256' ? 'SHA256' : m[6] === 'SHA512' ? 'SHA512' : m[6] === 'BLAKE2s' ? 'BLAKE2S' : 'BLAKE2B', functii: ['digest'] }, m[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,11 +352,13 @@ export function detecteazaRust(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// 8. openssl: versiunea minima/maxima TLS
|
||||
if ([...locale.values()].some((u) => u.crate === 'openssl') || /(?<![\w:])openssl::/.test(ctx.code)) {
|
||||
// 8. openssl (si boring / boring-signal, acelasi API): versiunea minima/maxima TLS
|
||||
const crateSsl = [...locale.values()].map((u) => u.crate).find((c) => /^(openssl|boring|boring_signal)$/.test(c))
|
||||
|| (/(?<![\w:])(openssl|boring_signal|boring)::/.exec(ctx.code) || [])[1] || null;
|
||||
if (crateSsl) {
|
||||
const vers = { SSL3: 'ssl3', TLS1: '1.0', TLS1_1: '1.1', TLS1_2: '1.2', TLS1_3: '1.3' };
|
||||
for (const m of ctx.potriviri(/\.\s*set_(min|max)_proto_version\s*\(\s*Some\s*\(\s*(?:openssl::ssl::)?SslVersion::(SSL3|TLS1_3|TLS1_2|TLS1_1|TLS1)\b/g)) {
|
||||
adauga('openssl', m.index, `openssl set_${m[1]}_proto_version`, 'config', { grup: 'TLS', param: vers[m[2]], rol: m[1] }, m[0]);
|
||||
for (const m of ctx.potriviri(/\.\s*set_(min|max)_proto_version\s*\(\s*Some\s*\(\s*(?:(?:openssl|boring|boring_signal)::ssl::)?SslVersion::(SSL3|TLS1_3|TLS1_2|TLS1_1|TLS1)\b/g)) {
|
||||
adauga(crateSsl, m.index, `${NUME_CARGO(crateSsl)} set_${m[1]}_proto_version`, 'config', { grup: 'TLS', param: vers[m[2]], rol: m[1] }, m[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,7 +403,7 @@ function activStatic(r, metoda, generic, ctx, m) {
|
||||
case 'blake3':
|
||||
return tip === 'Hasher' && /^(new|new_keyed|new_derive_key|default)$/.test(metoda) ? { grup: 'HASH', hash: 'BLAKE3', functii: ['digest'] } : null;
|
||||
case 'hmac': {
|
||||
if (!/^(Hmac|SimpleHmac)$/.test(tip) || !/^(new|new_from_slice|new_varkey|default)$/.test(metoda)) return null;
|
||||
if (!/^(Hmac|SimpleHmac|HmacReset|SimpleHmacReset)$/.test(tip) || !/^(new|new_from_slice|new_varkey|default)$/.test(metoda)) return null;
|
||||
const h = hashDinTip(generic);
|
||||
return h ? { grup: 'MAC', hash: h, functii: ['tag'], crateHash: crateHashDinTip(generic) } : { grup: 'UNKNOWN', motiv: generic ? `HMAC over "${generic.trim()}", a hash this tool does not resolve.` : 'HMAC whose hash is a type alias or an inferred type parameter, not visible at this call.' };
|
||||
}
|
||||
@ -474,13 +500,13 @@ function activStatic(r, metoda, generic, ctx, m) {
|
||||
if (tip === 'EcdsaKeyPair') { const c = /P(256|384)/.exec(ctx.code.slice(m.index, m.index + 200)); return { grup: 'ECDSA', curba: c ? `secp${c[1]}r1` : undefined, functii: fn(/generate/.test(metoda) ? 'keygen' : 'other') }; }
|
||||
if (tip === 'EphemeralPrivateKey' && metoda === 'generate') { const c = /(X25519|ECDH_P256|ECDH_P384)/.exec(ctx.code.slice(m.index, m.index + 200)); return c && c[1] !== 'X25519' ? { grup: 'ECDH', curba: c[1] === 'ECDH_P256' ? 'secp256r1' : 'secp384r1', functii: ['keygen'] } : { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }; }
|
||||
return null;
|
||||
case 'openssl': {
|
||||
case 'openssl': case 'boring': case 'boring_signal': {
|
||||
if (tip === 'MessageDigest') { const h = { md5: 'MD5', sha1: 'SHA1', sha224: 'SHA224', sha256: 'SHA256', sha384: 'SHA384', sha512: 'SHA512', sha3_224: 'SHA3-224', sha3_256: 'SHA3-256', sha3_384: 'SHA3-384', sha3_512: 'SHA3-512', ripemd160: 'RIPEMD160', shake_128: 'SHAKE128', shake_256: 'SHAKE256' }[metoda]; return h ? { grup: 'HASH', hash: h, functii: ['digest'] } : null; }
|
||||
if (tip === 'Cipher') { const k = /^(aes|des_ede3|des|rc4|rc2|bf|chacha20_poly1305|chacha20|camellia|sm4|aria)_?(\d{3})?_?(\w+)?$/.exec(metoda); if (!k) return null; const nume = k[1] === 'des_ede3' ? '3DES' : k[1] === 'bf' ? 'Blowfish' : k[1] === 'chacha20_poly1305' ? 'ChaCha20-Poly1305' : k[1] === 'chacha20' ? 'ChaCha20' : k[1].toUpperCase(); return { grup: 'CIPHER', nume, param: k[2], mod: k[3] ? k[3].replace(/_/g, '-') : undefined, functii: ['encrypt'] }; }
|
||||
if (tip === 'Rsa' && /^(generate|generate_with_e)$/.test(metoda)) { const b = /^\s*(\d{3,5})\s*(?:,|$)/.exec(argsDupa(ctx, m)); return { grup: 'RSA', param: b ? b[1] : undefined, functii: ['keygen'], bucataExtra: b ? ` ${b[1]}` : '' }; }
|
||||
if (tip === 'EcKey' && /^(generate|from_curve_name)$/.test(metoda)) { const c = /Nid::(X9_62_PRIME256V1|SECP256K1|SECP384R1|SECP521R1|SECP224R1|BRAINPOOL_P256R1|BRAINPOOL_P384R1|BRAINPOOL_P512R1)/.exec(ctx.code.slice(m.index, m.index + 300)); const curba = c ? ({ X9_62_PRIME256V1: 'secp256r1', SECP256K1: 'secp256k1', SECP384R1: 'secp384r1', SECP521R1: 'secp521r1', SECP224R1: 'secp224r1', BRAINPOOL_P256R1: 'brainpoolP256r1', BRAINPOOL_P384R1: 'brainpoolP384r1', BRAINPOOL_P512R1: 'brainpoolP512r1' })[c[1]] : undefined; return { grup: 'EC', curba, functii: ['keygen'] }; }
|
||||
if (tip === 'PKey') { const k = /^generate_(ed25519|ed448|x25519|x448)$/.exec(metoda); if (!k) return null; return /^ed/.test(k[1]) ? { grup: 'EDDSA', nume: k[1] === 'ed448' ? 'Ed448' : 'Ed25519', functii: ['keygen'] } : { grup: 'XDH', nume: k[1] === 'x448' ? 'X448' : 'X25519', functii: ['keygen'] }; }
|
||||
if (/^(SslConnector|SslAcceptor|SslContext)$/.test(tip) && /^(builder|mozilla_modern|mozilla_intermediate|mozilla_modern_v5|mozilla_intermediate_v5)$/.test(metoda)) return { grup: 'TLS', param: 'negotiated', nume: `openssl ${tip}` };
|
||||
if (/^(SslConnector|SslAcceptor|SslContext)$/.test(tip) && /^(builder|mozilla_modern|mozilla_intermediate|mozilla_modern_v5|mozilla_intermediate_v5)$/.test(metoda)) return { grup: 'TLS', param: 'negotiated', nume: `${NUME_CARGO(crate)} ${tip}` };
|
||||
return null;
|
||||
}
|
||||
case 'rustls': case 'tokio_rustls': case 'hyper_rustls': case 'native_tls':
|
||||
@ -503,8 +529,12 @@ function activStatic(r, metoda, generic, ctx, m) {
|
||||
if (tip === 'TorClient') return { grup: 'UNKNOWN', nume: 'Tor client (arti)', motiv: 'Tor circuit and channel cryptography is negotiated with the relays by the Tor protocol version in use, not visible in this file.' };
|
||||
return null;
|
||||
case 'libcrux_ml_kem': {
|
||||
const f = fn(/generate/.test(metoda) ? 'keygen' : /encaps/.test(metoda) ? 'encapsulate' : /decaps/.test(metoda) ? 'decapsulate' : 'other');
|
||||
// 2026-10-18 (libsignal): modulele kyber512/768/1024 ale lui libcrux sunt Kyber pre-standard (prekey-urile PQXDH de dinaintea ML-KEM)
|
||||
const ky = cale.map((s) => /^kyber(512|768|1024)$/.exec(s)).find(Boolean);
|
||||
if (ky) return { grup: 'PREPQ', nume: `Kyber-${ky[1]}`, primitiv: 'kem', functii: f, nota: 'libcrux kyber module: CRYSTALS-Kyber, the pre-standard form of ML-KEM.' };
|
||||
const k = /MlKem(512|768|1024)/.exec(tip) || /mlkem(512|768|1024)/.exec(cale.join('::'));
|
||||
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;
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}`);
|
||||
|
||||
27
crypto-inventory/test/fixturi/java/Libsignal.java
Normal file
27
crypto-inventory/test/fixturi/java/Libsignal.java
Normal file
@ -0,0 +1,27 @@
|
||||
// Fixtura: API-ul Java/Android al libsignal (org.signal.libsignal), 2026-10-18. Fisierul nu se compileaza; e doar text pentru scaner.
|
||||
// Ancora e importul org.signal.libsignal.*; fara el, numele scurte (ECKeyPair, HKDF, Aes256GcmSiv) nu inseamna libsignal.
|
||||
package fixturi;
|
||||
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair;
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair;
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType;
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle;
|
||||
import org.signal.libsignal.crypto.Aes256GcmSiv;
|
||||
import org.signal.libsignal.crypto.Aes256GcmEncryption;
|
||||
import org.signal.libsignal.crypto.Aes256Ctr32;
|
||||
|
||||
public class Libsignal {
|
||||
public void exemple(byte[] key, byte[] nonce, byte[] ikm, KEMKeyType tipDinParametru) throws Exception {
|
||||
ECKeyPair ec = ECKeyPair.generate(); // EXPECT: X25519 | quantum-vulnerable
|
||||
IdentityKeyPair id = IdentityKeyPair.generate(); // EXPECT: X25519 | quantum-vulnerable
|
||||
KEMKeyPair kem = KEMKeyPair.generate(KEMKeyType.KYBER_1024); // EXPECT: Kyber-1024 | quantum-safe
|
||||
KEMKeyPair kem2 = KEMKeyPair.generate(tipDinParametru); // EXPECT: libsignal KEM key pair | unknown
|
||||
byte[] okm = HKDF.deriveSecrets(ikm, "info".getBytes(), 32); // EXPECT: HKDF | quantum-safe
|
||||
Aes256GcmSiv siv = new Aes256GcmSiv(key); // EXPECT: AES-256-GCM-SIV | quantum-safe
|
||||
Aes256GcmEncryption gcm = new Aes256GcmEncryption(key, nonce, new byte[0]); // EXPECT: AES-256-GCM | quantum-safe
|
||||
Aes256Ctr32 ctr = new Aes256Ctr32(key, nonce, 0); // EXPECT: AES-256-CTR | quantum-safe
|
||||
PreKeyBundle bundle = new PreKeyBundle(1, 1, 2, ec.getPublicKey(), 3, ec.getPublicKey(), new byte[64], id.getPublicKey()); // EXPECT: libsignal session setup (X3DH/PQXDH) | unknown
|
||||
}
|
||||
}
|
||||
12
crypto-inventory/test/fixturi/java/LibsignalImport.java
Normal file
12
crypto-inventory/test/fixturi/java/LibsignalImport.java
Normal file
@ -0,0 +1,12 @@
|
||||
// Fixtura: un fisier care importa libsignal fara niciun apel recunoscut: importul e dovada slaba (unknown), o singura data.
|
||||
// Si un fisier FARA importul libsignal nu primeste nimic pentru aceleasi nume scurte (vezi negative/FaraLibsignal.java).
|
||||
package fixturi;
|
||||
|
||||
import org.signal.libsignal.protocol.SessionCipher; // EXPECT: libsignal (org.signal.libsignal) | unknown
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
|
||||
public class LibsignalImport {
|
||||
public Object cifru(Object store, SignalProtocolAddress adresa) {
|
||||
return new SessionCipher(store, adresa);
|
||||
}
|
||||
}
|
||||
@ -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) };
|
||||
}
|
||||
14
crypto-inventory/test/fixturi/js/libsignal-client.ts
Normal file
14
crypto-inventory/test/fixturi/js/libsignal-client.ts
Normal file
@ -0,0 +1,14 @@
|
||||
// Fixtura: un client al bibliotecii Signal pentru Node (@signalapp/libsignal-client), 2026-10-18. Ancora e importul pachetului.
|
||||
import * as SignalClient from '@signalapp/libsignal-client';
|
||||
import { PrivateKey, KEMKeyPair, hkdf, Aes256GcmSiv } from '@signalapp/libsignal-client';
|
||||
|
||||
export function chei(ikm: Buffer, key: Buffer) {
|
||||
const priv = PrivateKey.generate(); // EXPECT: X25519 | quantum-vulnerable
|
||||
const id = SignalClient.IdentityKeyPair.generate(); // EXPECT: X25519 | quantum-vulnerable
|
||||
const kem = KEMKeyPair.generate(); // EXPECT: Kyber-1024 | quantum-safe
|
||||
const okm = hkdf(32, ikm, Buffer.from('info'), null); // EXPECT: HKDF | quantum-safe
|
||||
const okm2 = SignalClient.hkdf(32, ikm, Buffer.from('info'), null); // EXPECT: HKDF | quantum-safe
|
||||
const siv = Aes256GcmSiv.new(key); // EXPECT: AES-256-GCM-SIV | quantum-safe
|
||||
const bundle = SignalClient.PreKeyBundle.new(1, 1, 2, priv.getPublicKey(), 3, priv.getPublicKey(), Buffer.alloc(64), id.publicKey); // EXPECT: libsignal session setup (X3DH/PQXDH) | unknown
|
||||
return { priv, id, kem, okm, okm2, siv, bundle };
|
||||
}
|
||||
15
crypto-inventory/test/fixturi/negative/FaraLibsignal.java
Normal file
15
crypto-inventory/test/fixturi/negative/FaraLibsignal.java
Normal file
@ -0,0 +1,15 @@
|
||||
// Fixtura negativa: aceleasi nume scurte ca in API-ul libsignal (ECKeyPair.generate, HKDF.deriveSecrets, new Aes256GcmSiv), dar FARA
|
||||
// importul org.signal.libsignal: nu sunt libsignal, deci nu produc nicio gasire. Fisierul nu se compileaza; e doar text pentru scaner.
|
||||
package fixturi;
|
||||
|
||||
import com.exemplu.chei.ECKeyPair;
|
||||
import com.exemplu.kdf.HKDF;
|
||||
import com.exemplu.cifru.Aes256GcmSiv;
|
||||
|
||||
public class FaraLibsignal {
|
||||
public Object exemple(byte[] key, byte[] ikm) {
|
||||
ECKeyPair ec = ECKeyPair.generate();
|
||||
byte[] okm = HKDF.deriveSecrets(ikm, "info".getBytes(), 32);
|
||||
return new Aes256GcmSiv(key);
|
||||
}
|
||||
}
|
||||
10
crypto-inventory/test/fixturi/negative/fara-libsignal.ts
Normal file
10
crypto-inventory/test/fixturi/negative/fara-libsignal.ts
Normal file
@ -0,0 +1,10 @@
|
||||
// Fixtura negativa: aceleasi nume scurte ca in API-ul @signalapp/libsignal-client (PrivateKey.generate, KEMKeyPair.generate, hkdf,
|
||||
// Aes256GcmSiv.new), dar importate din alta parte: nu sunt libsignal, deci nu produc nicio gasire.
|
||||
import { PrivateKey, KEMKeyPair, hkdf, Aes256GcmSiv } from '../chei-proprii';
|
||||
|
||||
export function chei(ikm: Buffer, key: Buffer) {
|
||||
const priv = PrivateKey.generate();
|
||||
const kem = KEMKeyPair.generate();
|
||||
const okm = hkdf(32, ikm, Buffer.from('info'), null);
|
||||
return { priv, kem, okm, siv: Aes256GcmSiv.new(key) };
|
||||
}
|
||||
30
crypto-inventory/test/fixturi/rust/libsignal.rs
Normal file
30
crypto-inventory/test/fixturi/rust/libsignal.rs
Normal file
@ -0,0 +1,30 @@
|
||||
// Fixtura: ce a scapat inventarului pe libsignal (2026-10-18): BoringSSL prin `boring` / `boring-signal` (stiva TLS a lui libsignal,
|
||||
// acelasi API ca openssl), HmacReset din hmac 0.13, tipul de mod adus prin `use cbc::Decryptor`, functia libera x25519(), modulele
|
||||
// kyber ale lui libcrux (Kyber pre-standard) si tiparul Noise hibrid hfs (X25519 + Kyber1024 in aceeasi strangere de mana).
|
||||
use boring_signal::ssl::{SslConnector, SslMethod, SslVersion};
|
||||
use boring::rsa::Rsa;
|
||||
use boring::hash::MessageDigest;
|
||||
use hmac::{HmacReset, Mac};
|
||||
use sha2::Sha256;
|
||||
use aes::Aes256;
|
||||
use cbc::Decryptor;
|
||||
use cbc::cipher::KeyIvInit;
|
||||
use x25519_dalek as x25519;
|
||||
use libcrux_ml_kem::kyber1024;
|
||||
use libcrux_ml_kem::mlkem768;
|
||||
use snow::Builder;
|
||||
|
||||
pub const NOISE_HFS: &str = "Noise_NKhfs_25519+Kyber1024_ChaChaPoly_SHA256"; // EXPECT: X25519+Kyber1024 (Noise hfs) | quantum-safe ; ChaCha20-Poly1305 | quantum-safe ; SHA-256 | quantum-safe
|
||||
|
||||
pub fn exemple(key: &[u8; 32], iv: &[u8; 16], secret: [u8; 32], randomness: [u8; 64]) {
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); // EXPECT: boring-signal SslConnector | unknown
|
||||
ssl.set_min_proto_version(Some(SslVersion::TLS1_3)).unwrap(); // EXPECT: TLSv1.3 | unknown
|
||||
let rsa = Rsa::generate(2048).unwrap(); // EXPECT: RSA-2048 | quantum-vulnerable
|
||||
let md = MessageDigest::sha256(); // EXPECT: SHA-256 | quantum-safe
|
||||
let mac = HmacReset::<Sha256>::new_from_slice(key).unwrap(); // EXPECT: HMAC-SHA-256 | quantum-safe
|
||||
let dec = Decryptor::<Aes256>::new(key.into(), iv.into()); // EXPECT: AES-256-CBC | quantum-safe
|
||||
let pk = x25519::x25519(secret, x25519::X25519_BASEPOINT_BYTES); // EXPECT: X25519 | quantum-vulnerable
|
||||
let kp = kyber1024::generate_key_pair(randomness); // EXPECT: Kyber-1024 | quantum-safe
|
||||
let kp2 = mlkem768::generate_key_pair(randomness); // EXPECT: ML-KEM-768 | quantum-safe
|
||||
let _ = (ssl, rsa, md, mac, dec, pk, kp, kp2, Builder::new(NOISE_HFS.parse().unwrap()));
|
||||
}
|
||||
@ -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');
|
||||
|
||||
@ -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 .
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
27
pq-gateway/dovezi/imagine-docker-2026-10-18.md
Normal file
27
pq-gateway/dovezi/imagine-docker-2026-10-18.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Imaginea Docker a gateway-ului, construita si masurata, 2026-10-18 03:31Z
|
||||
|
||||
Pana azi README-ul spunea "not yet measured by us: a build of this image and the OpenSSL version it carries", iar roadmap-ul (s.2a,
|
||||
Shield, punctul 2) o tinea NEMASURATA. Masurat pe rpc2 (docker 29.6.2, 2 nuclee), din octetii COMISI (`git archive HEAD
|
||||
cloud-gateway/pq-gateway/Dockerfile cloud-gateway/pq-gateway/pq-gateway.mjs`; pq-gateway.mjs sha256 `8602ac1afa2fe002...`,
|
||||
Dockerfile `9690e8d1df1a9dc2...`), cu scriptul `pqgw-imagine-rpc2.sh` si sonda `pqgw-sonda.mjs` (ambele pastrate langa acest fisier):
|
||||
|
||||
| ce | masurat |
|
||||
|---|---|
|
||||
| `docker build` | cod 0 in 6 s; imaginea `sha256:d1e6b588a74a...`, 62.467.140 de octeti, linux/amd64, `user=node` |
|
||||
| baza `node:24-alpine` (digest `sha256:44fce696f38b...`) | Node v24.22.0, **OpenSSL 3.5.8** (`docker run --rm node:24-alpine node -p process.versions.openssl`) |
|
||||
| pornirea, `hybrid-only` | containerul `Up`; randul `listening`: `mode hybrid-only, groups X25519MLKEM768, minTlsVersion TLSv1.3`, plus avertismentul ca amontele e http simplu |
|
||||
| client care ofera NUMAI `X25519MLKEM768` (Node 24, OpenSSL 3.5.8, de pe aceeasi gazda) | **handshake reusit**: TLSv1.3, TLS_AES_256_GCM_SHA384, grupul raportat `X25519MLKEM768`, 16 ms |
|
||||
| client care ofera NUMAI `X25519`, in `hybrid-only` (control negativ) | **refuzat**: `ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE`, 18 ms |
|
||||
| `hybrid-preferred`: client numai `X25519` | reusit (TLSv1.3, grup `X25519`) |
|
||||
| `hybrid-preferred`: client `X25519MLKEM768:X25519` | reusit, grupul ales `X25519MLKEM768` (hibridul e preferat) |
|
||||
| RPC-ul public local al lui rpc2 inainte / dupa | 9 ms / 10 ms (`eth_blockNumber`) |
|
||||
| curatenie | containerul, imaginea gateway-ului si imaginea de baza sterse; 0 containere, 0 imagini straine ramase; porturile 8444/8081 libere |
|
||||
|
||||
Certificatul probei: EC P-256 autosemnat, 2 zile, citibil de uid 1000 (`chmod 644`); amontele probei: `python3 -m http.server` pe
|
||||
127.0.0.1:8081. Cererea HTTP prin gateway cu `curl --curves X25519MLKEM768` NU s-a masurat (optiunea `%{tls_version}` lipseste in
|
||||
curl-ul gazdei; nu e o constatare despre gateway).
|
||||
|
||||
Doua capcane platite in aceeasi rulare, fara paguba: (1) randul `listening` a fost taiat la 220 de coloane de `cut`, deci versiunea
|
||||
OpenSSL a gateway-ului a lipsit din jurnalul adus si s-a masurat separat pe imaginea de baza; (2) serverul de proba pornit cu
|
||||
`nohup ... &` dintr-un subshell a ramas in viata dupa script (`$!` scris intr-un fisier relativ sters de curatenie), a tinut sesiunea
|
||||
ssh deschisa 10 minute dupa ce scriptul se terminase (aceeasi familie cu C5, 2026-10-11) si a fost oprit de mana; nimic nu a ramas armat.
|
||||
34
pq-gateway/dovezi/pqgw-imagine-rpc2.sh
Normal file
34
pq-gateway/dovezi/pqgw-imagine-rpc2.sh
Normal file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
# pqgw-imagine-rpc2.sh (PE rpc2; fisierele vin in /root/pqgw-imagine: pqgw.tar din `git archive HEAD` + pqgw-sonda.mjs):
|
||||
# masoara ce README-ul gateway-ului numeste NEMASURAT: imaginea Docker se construieste, PORNESTE, poarta OpenSSL >= 3.5, si un client
|
||||
# care ofera NUMAI X25519MLKEM768 face handshake, iar unul clasic e REFUZAT in modul hybrid-only (controlul negativ). Totul pe 127.0.0.1,
|
||||
# port 8444, container si imagine sterse la sfarsit (nimic ramane armat). RPC-ul local e cronometrat inainte si dupa.
|
||||
set -u
|
||||
D=/root/pqgw-imagine; cd "$D" || exit 2
|
||||
N=/opt/node24/bin/node
|
||||
rpc() { local t0=$(date +%s%N); curl -s -m 5 -H 'content-type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' http://127.0.0.1:8545 | grep -o '"result":"0x[0-9a-f]*"' | head -1; echo " ($(( ($(date +%s%N) - t0) / 1000000 )) ms)"; }
|
||||
echo "== RPC inainte: $(rpc)"
|
||||
rm -rf src && mkdir -p src certs && tar -xf pqgw.tar -C src && ls -la src/cloud-gateway/pq-gateway | sed -n '2,5p'
|
||||
sha256sum src/cloud-gateway/pq-gateway/pq-gateway.mjs src/cloud-gateway/pq-gateway/Dockerfile | cut -c1-16,66-
|
||||
echo "== docker build"; t0=$(date +%s); docker build -q -t aere-pq-gateway:proba src/cloud-gateway/pq-gateway > build.log 2>&1; c=$?; echo "build cod=$c in $(( $(date +%s) - t0 )) s; $(tail -1 build.log | cut -c1-100)"
|
||||
[ $c -eq 0 ] || { tail -20 build.log; exit 1; }
|
||||
docker image inspect aere-pq-gateway:proba --format 'imagine: {{.Id}} {{.Size}} octeti, {{.Os}}/{{.Architecture}}, user={{.Config.User}}'
|
||||
echo "== certificat de proba (EC P-256, autosemnat, citibil de uid 1000)"
|
||||
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout certs/privkey.pem -out certs/fullchain.pem -days 2 -subj '/CN=proba.local' > /dev/null 2>&1 && chmod 644 certs/privkey.pem certs/fullchain.pem && echo "cert ok"
|
||||
echo "== upstream de proba (python http.server pe 127.0.0.1:8081)"; (cd certs && nohup python3 -m http.server 8081 --bind 127.0.0.1 > /dev/null 2>&1 & echo $! > ../upstream.pid); sleep 1
|
||||
docker rm -f pqgw-proba > /dev/null 2>&1
|
||||
echo "== pornire: hybrid-only"
|
||||
docker run -d --name pqgw-proba --network host -e AERE_PQGW_LISTEN=127.0.0.1:8444 -e AERE_PQGW_MODE=hybrid-only -e AERE_PQGW_UPSTREAM=http://127.0.0.1:8081 -v "$D/certs:/etc/aere-pq-gateway:ro" aere-pq-gateway:proba > /dev/null; sleep 3
|
||||
docker ps --filter name=pqgw-proba --format 'container: {{.Status}}'
|
||||
echo "-- jurnal (randul listening cu OpenSSL)"; docker logs pqgw-proba 2>&1 | head -5 | cut -c1-220
|
||||
echo "-- sonda hibrid singur (trebuie ok)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519MLKEM768; echo "cod=$?"
|
||||
echo "-- sonda clasic singur in hybrid-only (CONTROL NEGATIV: trebuie refuzat)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519; echo "cod=$?"
|
||||
echo "-- o cerere HTTP prin gateway (hibrid) spre upstream"; curl -sk -m 8 --curves X25519MLKEM768 --tlsv1.3 https://127.0.0.1:8444/ -o /dev/null -w 'http=%{http_code} ssl=%{ssl_verify_result} tls=%{tls_version}\n' 2>&1 | head -1 || true
|
||||
echo "== repornire: hybrid-preferred (clasicul trebuie sa mearga, hibridul preferat)"
|
||||
docker rm -f pqgw-proba > /dev/null 2>&1
|
||||
docker run -d --name pqgw-proba --network host -e AERE_PQGW_LISTEN=127.0.0.1:8444 -e AERE_PQGW_MODE=hybrid-preferred -e AERE_PQGW_UPSTREAM=http://127.0.0.1:8081 -v "$D/certs:/etc/aere-pq-gateway:ro" aere-pq-gateway:proba > /dev/null; sleep 3
|
||||
echo "-- sonda clasic singur in hybrid-preferred (trebuie ok)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519; echo "cod=$?"
|
||||
echo "-- sonda hibrid+clasic in hybrid-preferred (trebuie ok)"; $N pqgw-sonda.mjs 127.0.0.1 8444 X25519MLKEM768:X25519; echo "cod=$?"
|
||||
docker logs pqgw-proba 2>&1 | tail -4 | cut -c1-220
|
||||
echo "== curatenie"; docker rm -f pqgw-proba > /dev/null 2>&1; docker rmi aere-pq-gateway:proba > /dev/null 2>&1 && echo "imaginea stearsa"; kill "$(cat upstream.pid)" 2>/dev/null; rm -rf certs upstream.pid; docker ps --filter name=pqgw --format '{{.Names}}' | grep -c . | sed 's/^/containere pqgw ramase: /'
|
||||
echo "== RPC dupa: $(rpc)"
|
||||
14
pq-gateway/dovezi/pqgw-sonda.mjs
Normal file
14
pq-gateway/dovezi/pqgw-sonda.mjs
Normal file
@ -0,0 +1,14 @@
|
||||
// pqgw-sonda.mjs <gazda> <port> <grupuri>: o strangere de mana TLS 1.3 reala cu grupurile de schimb de cheie date (de ex.
|
||||
// X25519MLKEM768 singur, sau X25519 singur), fara verificarea certificatului (cel al probei e autosemnat). Tipareste JSON cu
|
||||
// protocolul, cifrul, grupul raportat de OpenSSL (pentru hibrid OpenSSL nu il numeste; reusita cu grupul hibrid OFERIT SINGUR e dovada)
|
||||
// si motivul esecului. Iesire 0 = handshake reusit, 1 = refuzat.
|
||||
import tls from 'node:tls';
|
||||
const [gazda, port, grupuri] = process.argv.slice(2);
|
||||
const t0 = Date.now();
|
||||
const s = tls.connect({ host: gazda, port: Number(port), servername: 'proba.local', rejectUnauthorized: false, minVersion: 'TLSv1.3', maxVersion: 'TLSv1.3', ecdhCurve: grupuri, ALPNProtocols: ['http/1.1'] }, () => {
|
||||
const ek = s.getEphemeralKeyInfo();
|
||||
console.log(JSON.stringify({ ok: true, grupuriOferite: grupuri, protocol: s.getProtocol(), cifru: s.getCipher().name, grupRaportat: ek && ek.name, alpn: s.alpnProtocol, ms: Date.now() - t0, openssl: process.versions.openssl }));
|
||||
s.end(); process.exitCode = 0;
|
||||
});
|
||||
s.setTimeout(8000, () => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, motiv: 'timeout' })); s.destroy(); process.exitCode = 1; });
|
||||
s.on('error', (e) => { console.log(JSON.stringify({ ok: false, grupuriOferite: grupuri, motiv: e.code || e.message, ms: Date.now() - t0, openssl: process.versions.openssl })); process.exitCode = 1; });
|
||||
Loading…
Reference in New Issue
Block a user