From f971fa9e287673b866f52a3f1a2cffa572d55469 Mon Sep 17 00:00:00 2001 From: Aere Network Date: Sun, 18 Oct 2026 05:59:35 +0300 Subject: [PATCH] crypto-inventory 0.3.0: glob imports resolved, libcrux and spqr, libsignal KEM key pairs; control-plane: a key agreement or client-side TLS called by the application itself is not routed to the TLS gateway (planner 35/35, B1 negative control 4/4) --- README.md | 4 +- control-plane/README.md | 9 +++ control-plane/control-plane.mjs | 3 + control-plane/plan-migrare.mjs | 34 ++++++++- control-plane/proba-control-plane.mjs | 5 +- control-plane/proba-plan-migrare.mjs | 14 ++++ crypto-inventory/README.md | 2 +- crypto-inventory/lib/detect-rust.mjs | 87 +++++++++++++++++----- crypto-inventory/test/control-negativ.mjs | 4 +- crypto-inventory/test/fixturi/rust/glob.rs | 16 ++++ crypto-inventory/test/proba.mjs | 13 +++- 11 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 crypto-inventory/test/fixturi/rust/glob.rs diff --git a/README.md b/README.md index 3e80626..f5b0ed1 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j | verify-layer | 45/45 with the AIP-23 reference verifier, measured 2026-10-05 (`AERE_VERIFY_PROOF= node proba-sidecar.mjs`); without it 40 run, 5 are reported as skipped and the exit code is 2; the Merkle tree 20/20 (`node proba-arbore.mjs`: the reference roots of Certificate Transparency, every proof up to 64 leaves, each attack), measured 2026-09-30 | 10/10 in this repository on 2026-10-05 (`node control-negativ-sidecar.mjs`; its eleventh case compares with the version from the development history and is skipped here); the tree 14/14 (`node control-negativ-arbore.mjs`) | | proof-kinds | 24/24 with the same verifier (`AERE_VERIFY_PROOF=... node proba-proof-kinds.mjs`) | six negative controls inside the test | | readiness | 6/6 (`node proba-adrese-private.mjs`: the private-address rules, and a local listener no scan may touch) | the rate limit and the queue bound are tested where the service runs, not here (its README says so) | -| control-plane | planner 30/30, 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 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`) | | identity | 46/46 (`node proba-identity.mjs`, with the AIP-23 verifier for its envelope test; without it that test is reported as skipped and the exit code is 2), compliance 15/15 (`node proba-conformitate.mjs`), Travel Rule 19/19 (`node proba-travel-rule.mjs`), SD-JWT 23/23 (`node proba-sdjwt.mjs`, the RFC 9901 vectors included), measured 2026-10-05 | 53/53 (`node control-negativ-identity.mjs`); compliance 14/14 (`node control-negativ-conformitate.mjs`); Travel Rule 19/19 (`node control-negativ-travel-rule.mjs`); SD-JWT 29/29 (`node control-negativ-sdjwt.mjs`) | | agents | policy 27/27 with the AIP-23 verifier (without it 25 run, 2 are reported as skipped and the exit code is 2), ledger 54/54 on 2026-10-05, approval and revocation 39/39, the chain as witness 26/26 without a network and 7/7 on testnet 28001 on 2026-09-30 (`proba-agent-ancora-testnet.mjs`, needs a funded testnet key and the AIP-23 verifier), command line 23/23 through files and processes only (on Linux and macOS one more test checks the key file mode; not measured here); x402 wallet 25/25, 2-of-2 co-signing 16/16 and payment verifier 14/14 without a network; on the public testnet 28001, 9/9 with the wallet key and 13/13 with the 2-of-2 contract wallet (`agents/x402/proba-x402-testnet.mjs`, `agents/x402/proba-cosign-testnet.mjs`, each needs a funded testnet key); the contract's artifact recompiles byte for byte with solc 0.8.23 (`node x402/recompileaza-contract.mjs --solc `) | 26/26 (`node control-negativ-aprobare.mjs`); the chain as witness 11/11 (`node control-negativ-ancora.mjs`); x402 30/30 (`node x402/control-negativ-wallet.mjs`); the contract's own tests (7) and their negative control (4/4) run in the Aere Network contracts project, not in this repository | | proof-of-ai | 6/6 with the AIP-23 verifier (`AERE_VERIFY_PROOF=... node proba-proof-of-ai.mjs`); without it the probe reports NEMASURAT and exits 2 | one negative control inside the test (an envelope touched after its hash: INVALID) | @@ -62,4 +62,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## Licence -MIT, see [LICENSE](LICENSE). Files: 705 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 47, 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: 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). diff --git a/control-plane/README.md b/control-plane/README.md index 4623575..27d957a 100644 --- a/control-plane/README.md +++ b/control-plane/README.md @@ -15,6 +15,15 @@ documentation are in English (the data format was translated on 2026-09-29, vers Runs the inventory on a code directory and, optionally, takes a scan of a hostname (the JSON of the readiness scanner), and writes a prioritized plan. Every quantum-vulnerable asset becomes an action: +Since 2026-10-18 (first run on a code base that is not ours, a Rust messenger): a key agreement that the +application calls itself (X25519 in a ratchet, a PQXDH handshake, a Tor `ntor` handshake, `createECDH` in +Node) is **not** routed to the PQ Gateway, because a TLS edge cannot see it; its action is `manual`, in +code, with the hybrid target written out (X25519 + ML-KEM-768 in the same handshake and key schedule). A +signature or public-key encryption that the application performs itself keeps its PKI/KMS action (the +product issues the hybrid key, for real), and `how` says that the call site must then be changed to use it. +Only a finding with no call context (no `api`, no language) keeps the old routing: its context is not +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 } diff --git a/control-plane/control-plane.mjs b/control-plane/control-plane.mjs index 621fd25..406409a 100644 --- a/control-plane/control-plane.mjs +++ b/control-plane/control-plane.mjs @@ -41,6 +41,9 @@ function laPlanificator(g) { curve: g.curve, parameterSetIdentifier: g.parameterSetIdentifier, certificate: g.certificate, material: g.material, + // 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, location: g.file ? `${g.file}:${g.line || '?'}` : undefined, }; } diff --git a/control-plane/plan-migrare.mjs b/control-plane/plan-migrare.mjs index 5b25d03..4c3aef2 100644 --- a/control-plane/plan-migrare.mjs +++ b/control-plane/plan-migrare.mjs @@ -27,6 +27,32 @@ const TINTE = { pke: { target: 'ML-KEM-768 (hybrid KEM) instead of public-key encryption', product: 'kms', how: 'hybrid envelope through the KMS' }, }; +// 2026-10-18 (prima rulare pe un arbore strain, Avano): un X25519, un Ed25519 sau un RSA chemat de APLICATIE (ratchet, PQXDH, +// ntor, semnaturi de mesaj, chei derivate) nu se repara punand un gateway TLS in fata serviciului si nici emitand un certificat: +// gateway-ul vede numai TLS-ul, PKI-ul numai certificatele. Numai o folosire de TLS/certificat din cod se trimite la produs; restul +// e lucru IN COD, manual, cu tinta hibrida scrisa. O constatare fara `api` (ECDH generic, fara context) ramane pe drumul vechi: +// contextul ei nu se stie, nu se ghiceste. +const IN_TLS = /\b(tls|ssl|https|x509|SSLContext|SslContext|ecdhCurve|CurvePreferences|namedGroups|KeyStore|keytool|rustls|SslConnector|SslAcceptor|TlsConnector|TlsAcceptor|HttpsConnectorBuilder)\b/i; +const TINTE_IN_COD = { + 'key-agree': { target: 'X25519 + ML-KEM-768 in the same handshake and key schedule (hybrid KEM, as PQXDH does)', product: null, + how: 'called by the application itself, not by a TLS edge: a gateway in front cannot change it. Combine it with ML-KEM-768 in the protocol (both shared secrets into the KDF); if the protocol already pairs it with a post-quantum KEM in the same handshake, record that as the evidence and close this action.' }, +}; +// semnaturile si cifrarea cu cheie publica din cod raman pe PKI/KMS (produsul emite cheia hibrida, real, prin executor), dar `how` +// spune ca APLICATIA trebuie sa o adopte: produsul da cheia, nu schimba apelul din cod +const NOTA_ADOPTARE = { + signature: ' The application signs with its own key here, so after the PQ PKI issues the hybrid key and certificate the signing call must be changed to use them (a code change; the PKI provides the key, not the call site).', + pke: ' The application encrypts with its own public key here, so after the KMS holds the hybrid key the call must be changed to use the KMS envelope (a code change; the KMS provides the key, not the call site).', +}; +// TLS de CLIENT configurat in aplicatie: gateway-ul (terminare de server) nu schimba ce ofera clientul; se repara in client +const TLS_CLIENT = /\b(default_provider|TlsConnector|ClientConfig|HttpsConnectorBuilder|SslConnector)\b/; +function tlsClientInCod(f) { + return !!(f.language && f.api) && TLS_CLIENT.test(String(f.api)) && f.assetType !== 'certificate' && f.assetType !== 'related-crypto-material'; +} +function inCod(f) { + return !!(f.language && f.api) && !IN_TLS.test(String(f.api)) && f.protocolType !== 'tls' + && f.assetType !== 'certificate' && f.assetType !== 'related-crypto-material'; +} + // clasificarea unui asset vulnerabil in urgenta, pe natura primitivei (HNDL vs falsificare) function urgenta(prim, expusPublic, certLunga) { if (prim === 'key-agree' || prim === 'kem' || prim === 'pke') return expusPublic ? 'CRITICAL' : 'HIGH'; // HNDL: datele de azi se recolteaza @@ -68,7 +94,13 @@ function actiuneDinInventar(f) { const prim = primitivaDe(f); const certLunga = f.assetType === 'certificate' && f.certificate && f.certificate.notValidAfter && (new Date(f.certificate.notValidAfter) - new Date()) > 365 * 24 * 3600 * 1000; - const t = TINTE[prim] || { target: 'hybrid post-quantum equivalent', product: null, how: 'to be decided' }; + const eInCod = inCod(f); + const tInCod = (eInCod && TINTE_IN_COD[prim]) + || (tlsClientInCod(f) && (prim === 'key-agree' || prim === 'kem') && { target: 'X25519MLKEM768 offered by this client (hybrid)', product: null, + how: 'client-side TLS configured by the application itself: a PQ Gateway in front of a server does not change what this client offers. Switch the client to a provider that offers X25519MLKEM768 (rustls: aws-lc-rs, 0.23.22 or later; OpenSSL 3.5 or later) or enable the hybrid group in its configuration, then re-run the inventory.' }); + const t0 = TINTE[prim] || { target: 'hybrid post-quantum equivalent', product: null, how: 'to be decided' }; + const t = tInCod ? { ...tInCod, how: `${f.name || prim} via ${f.api}: ${tInCod.how}` } + : (eInCod && NOTA_ADOPTARE[prim] ? { ...t0, how: t0.how + NOTA_ADOPTARE[prim] } : t0); const numeAsset = f.assetType === 'certificate' ? `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 + ')' : ''}`; diff --git a/control-plane/proba-control-plane.mjs b/control-plane/proba-control-plane.mjs index 1f49785..873e4bb 100644 --- a/control-plane/proba-control-plane.mjs +++ b/control-plane/proba-control-plane.mjs @@ -38,8 +38,9 @@ try { cer('dosar vulnerabil: 2 actiuni in plan', pv.summary.total === 2); const ecdh = pv.actions.find((a) => /ECDH/i.test(a.name || a.asset)); const rsa = pv.actions.find((a) => /RSA/i.test(a.name || a.asset)); - cer('ECDH -> gateway (schimb de cheie, auto)', ecdh && ecdh.product === 'gateway' && ecdh.method === 'auto-aere'); - cer('RSA -> pki (semnatura, auto)', rsa && rsa.product === 'pki'); + // 2026-10-18: createECDH e schimb de cheie facut de APLICATIE (api-ul si limbajul trec prin adaptor): manual, in cod, tinta hibrida, fara gateway + cer('ECDH din cod (createECDH) -> manual, in cod, tinta X25519 + ML-KEM-768, fara gateway', ecdh && ecdh.method === 'manual' && !ecdh.product && /ML-KEM-768/.test(ecdh.target) && /createECDH/.test(ecdh.how)); + cer('RSA -> pki (semnatura, auto), cu nota ca aplicatia adopta cheia (schimbare de cod)', rsa && rsa.product === 'pki' && /code change/.test(rsa.how)); cer('HNDL: ECDH mai urgent decat RSA', ecdh && rsa && ({ CRITICAL: 0, HIGH: 1, MEDIUM: 2 }[ecdh.urgency] < { CRITICAL: 0, HIGH: 1, MEDIUM: 2 }[rsa.urgency])); cer('fiecare actiune are locatia (fisier:linie)', pv.actions.every((a) => a.location && /\.js:\d/.test(a.location))); diff --git a/control-plane/proba-plan-migrare.mjs b/control-plane/proba-plan-migrare.mjs index 319c749..c665039 100644 --- a/control-plane/proba-plan-migrare.mjs +++ b/control-plane/proba-plan-migrare.mjs @@ -19,6 +19,11 @@ const inventory = [ { ref: 'algorithm:mlkem', assetType: 'algorithm', name: 'ML-KEM-768', primitive: 'kem', quantumVulnerable: false }, { ref: 'algorithm:mldsa', assetType: 'algorithm', name: 'ML-DSA-65', primitive: 'signature', quantumVulnerable: false }, { ref: 'algorithm:aes', assetType: 'algorithm', name: 'AES-256-GCM', primitive: 'ae', quantumVulnerable: false }, + // 2026-10-18 (Avano): criptografie chemata de aplicatie, cu limbajul si API-ul din inventarul real; si un ECDH din configuratia TLS a codului + { ref: 'cod:x25519:rust', assetType: 'algorithm', name: 'X25519', primitive: 'key-agree', quantumVulnerable: true, language: 'rust', api: 'StaticSecret::random_from_rng', location: 'src/ratchet.rs:10' }, + { 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-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 @@ -49,6 +54,15 @@ cer('certificatul WebPKI public e BLOCAT (niciun CA public PQ), nu se pretinde a cer('scanarea: schimbul de cheie clasic e in plan, auto prin gateway', byRef['tls-kex:client.exemplu.com'] && byRef['tls-kex:client.exemplu.com'].product === 'gateway'); cer('scanarea: TLS 1.2 e in plan, manual (config)', byRef['tls-conf:client.exemplu.com:tls12-accepted'] && byRef['tls-conf:client.exemplu.com:tls12-accepted'].method === 'manual'); +// 2026-10-18 (Avano): criptografia chemata de aplicatie nu se repara cu gateway-ul sau cu PKI-ul; cea din configuratia TLS, da +const xc = byRef['cod:x25519:rust'], ec = byRef['cod:ed25519:rust'], tc = byRef['cod:tls:go']; +cer('X25519 chemat din cod (ratchet) -> manual, in cod, tinta hibrida cu ML-KEM-768, fara gateway', xc && xc.method === 'manual' && !xc.product && /ML-KEM-768/.test(xc.target) && /gateway in front cannot/.test(xc.how) && /StaticSecret/.test(xc.how)); +cer('Ed25519 pe date de aplicatie -> PKI emite cheia hibrida, iar how spune ca aplicatia trebuie sa o adopte (schimbare de cod)', ec && ec.product === 'pki' && /ML-DSA/.test(ec.target) && /code change/.test(ec.how) && /call site/.test(ec.how)); +cer('CONTROL: ECDH din configuratia TLS a codului ramane auto prin gateway', tc && tc.product === 'gateway' && tc.method === 'auto-aere'); +cer('HNDL ramane si in cod: X25519 HIGH, Ed25519 MEDIUM', xc && ec && xc.urgency === 'HIGH' && ec.urgency === 'MEDIUM'); +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)); + // controale negative: ce NU trebuie sa fie in plan cer('CONTROL: ML-KEM (deja PQ) NU e in plan', !byRef['algorithm:mlkem']); cer('CONTROL: ML-DSA (deja PQ) NU e in plan', !byRef['algorithm:mldsa']); diff --git a/crypto-inventory/README.md b/crypto-inventory/README.md index 73c7c71..7dc50e6 100644 --- a/crypto-inventory/README.md +++ b/crypto-inventory/README.md @@ -23,7 +23,7 @@ An API name that appears only inside a comment or inside a string literal is not | 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`, ...) | | Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) | -| Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::`, `Hkdf::`, `type HmacSha256 = Hmac`, `SigningKey::`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor`, `cbc::Decryptor`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr` | +| Rust | `use`/`extern crate` resolution of names to crates; static calls `Type::method` (`Sha256::new`, `SigningKey::generate`, `Aes256Gcm::new_from_slice`, `RsaPrivateKey::new(rng, 2048)`, `MlDsa65::key_gen`, `MlKem768::generate`); generic parameters (`Hmac::`, `Hkdf::`, `type HmacSha256 = Hmac`, `SigningKey::`); path functions (`blake3::hash`, `pbkdf2::pbkdf2_hmac::`, `pqcrypto_kyber::kyber1024::keypair`, `rustls::crypto::ring::default_provider`); `ring` constants (`aead::AES_256_GCM`, `signature::ED25519`, `agreement::X25519`); `jsonwebtoken::Algorithm::*`, `oqs::Algorithm::*`, libsignal `kem::KeyType::Kyber1024`; RustCrypto modes (`ecb::Encryptor`, `cbc::Decryptor`); `snow` Noise patterns (`Noise_XX_25519_ChaChaPoly_SHA256`); `openssl` (`MessageDigest::*`, `Cipher::*`, `Rsa::generate`, `EcKey::generate` + `Nid`, `PKey::generate_ed25519`, `set_min_proto_version`); `secp256k1` `.sign_ecdsa`/`.sign_schnorr`; libcrux (`libcrux_ml_kem::mlkem1024::generate_key_pair`, `libcrux_ml_dsa::ml_dsa_65::sign`); a name brought in by a glob import (`use libsignal_protocol::*`) is resolved by trying the glob-imported crates in order | | 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 | diff --git a/crypto-inventory/lib/detect-rust.mjs b/crypto-inventory/lib/detect-rust.mjs index f9cef6e..e33fb05 100644 --- a/crypto-inventory/lib/detect-rust.mjs +++ b/crypto-inventory/lib/detect-rust.mjs @@ -60,6 +60,11 @@ const CRATE = { pqcrypto_hqc: { ofera: 'HQC', grupuri: ['HQC'] }, pqcrypto_classicmceliece: { ofera: 'Classic McEliece (NIST round 4, not standardized)', grupuri: ['UNKNOWN'] }, oqs: { ofera: 'liboqs: post-quantum KEMs and signatures', grupuri: ['MLKEM', 'MLDSA', 'FALCON', 'PREPQ', 'HQC', 'UNKNOWN'] }, + libcrux_ml_kem: { ofera: 'ML-KEM (libcrux, formally verified; used under libsignal)', grupuri: ['MLKEM'] }, + libcrux_ml_dsa: { ofera: 'ML-DSA (libcrux)', grupuri: ['MLDSA'] }, + libcrux_sha3: { ofera: 'SHA3 and SHAKE (libcrux)', grupuri: ['HASH'] }, + libcrux_kem: { ofera: 'KEMs (libcrux: ML-KEM, X25519, hybrids)', grupuri: ['MLKEM', 'XDH', 'HYBRID-KEX'] }, + spqr: { ofera: "Signal's post-quantum ratchet (SPQR, ML-KEM based)", grupuri: ['MLKEM'] }, 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'] }, @@ -90,6 +95,7 @@ for (const [crate, tipuri] of Object.entries({ x25519_dalek: ['StaticSecret', 'EphemeralSecret', 'ReusableSecret'], curve25519_dalek: ['RistrettoPoint', 'EdwardsPoint', 'MontgomeryPoint'], ml_kem: ['MlKem512', 'MlKem768', 'MlKem1024'], ml_dsa: ['MlDsa44', 'MlDsa65', 'MlDsa87'], + libcrux_ml_kem: ['MlKem512KeyPair', 'MlKem768KeyPair', 'MlKem1024KeyPair', 'MlKem512PublicKey', 'MlKem768PublicKey', 'MlKem1024PublicKey', 'MlKem512PrivateKey', 'MlKem768PrivateKey', 'MlKem1024PrivateKey', 'MlKem512Ciphertext', 'MlKem768Ciphertext', 'MlKem1024Ciphertext'], libsignal_protocol: ['PreKeyBundle', 'IdentityKeyPair', 'SignalProtocolAddress', 'KyberPreKeyRecord'], arti_client: ['TorClient'], })) for (const t of tipuri) UNIC[t] = crate; @@ -184,36 +190,46 @@ export function detecteazaRust(ctx) { ctx.adauga(pos, api, fel, curat, { bucata }); }; - // o cale scrisa in cod (prefixul `a::b::`) sau un nume de tip -> calea completa prin `use` - const rezolvaCale = (prefix, tip) => { + // o cale scrisa in cod (prefixul `a::b::`) sau un nume de tip -> caile posibile, in ordine: prin `use`, prin numele unic al + // tipului, apoi prin fiecare crate adus cu `use crate::*` (un import glob nu spune ce nume aduce; se incearca si se accepta + // primul care are un inteles acolo). Un prefix din propriul crate (crate::, self::, super::) nu se incearca nicaieri. + const caiPentru = (seg) => { + const r = []; + if (CRATE[seg[0]]) r.push(seg); + for (const g of globuri) if (CRATE[g]) r.push([g, ...seg]); + return r; + }; + const candidati = (prefix, tip) => { const seg = prefix ? prefix.split('::').filter(Boolean) : []; if (seg.length) { - if (RADACINI_PROPRII.has(seg[0])) return null; + if (RADACINI_PROPRII.has(seg[0])) return []; const u = locale.get(seg[0]); - const cale = u ? [...u.cale, ...seg.slice(1)] : seg; - return CRATE[cale[0]] ? { crate: cale[0], cale: [...cale, tip] } : null; + if (u) return CRATE[u.crate] ? [{ crate: u.crate, cale: [...u.cale, ...seg.slice(1), tip] }] : []; + return caiPentru(seg).map((c) => ({ crate: c[0], cale: [...c, tip] })); } const u = locale.get(tip); - if (u) return CRATE[u.crate] ? { crate: u.crate, cale: u.cale } : null; - if (UNIC[tip]) return { crate: UNIC[tip], cale: [UNIC[tip], tip] }; - return null; + if (u) return CRATE[u.crate] ? [{ crate: u.crate, cale: u.cale }] : []; + if (UNIC[tip]) return [{ crate: UNIC[tip], cale: [UNIC[tip], tip] }]; + return [...globuri].filter((g) => CRATE[g]).map((g) => ({ crate: g, cale: [g, tip] })); }; + const rezolvaCale = (prefix, tip) => candidati(prefix, tip)[0] || null; // 1. apelurile statice Tip::metoda, cu sau fara turbofish (Hmac::::new_from_slice) const RE_STATIC = /(?)?\s*::\s*([a-z_]\w*)\b/g; for (const m of ctx.potriviri(RE_STATIC)) { const [intreg, prefix, tip, generic, metoda] = m; - const r = rezolvaCale(prefix, tip); + const cand = candidati(prefix, tip); + let r = null; let activ = null; + for (const c of cand) { const a = activStatic(c, metoda, generic, ctx, m); if (a) { r = c; activ = a; break; } } if (!r) { - if (!prefix && /^(SigningKey|VerifyingKey|KeyPair|Keypair|SecretKey|PublicKey|Signature)$/.test(tip) && CTOR.test(metoda) && !locale.has(tip) && !proprii.has(tip)) { + if (!cand.length && !prefix && /^(SigningKey|VerifyingKey|KeyPair|Keypair|SecretKey|PublicKey|Signature)$/.test(tip) && CTOR.test(metoda) && !locale.has(tip) && !proprii.has(tip)) { // nume ambiguu, fara `use` care sa spuna crate-ul: nu se ghiceste const tipuriLocale = new RegExp(`\\b(struct|enum|type|trait)\\s+${tip}\\b`).test(ctx.code); if (!tipuriLocale) ctx.adauga(m.index, `${tip}::${metoda}`, 'call', { grup: 'UNKNOWN', nume: `${tip} (crate not resolved)`, motiv: `${tip}::${metoda} with no \`use\` in this file naming its crate: it may be Ed25519 (ed25519-dalek), ML-DSA (ml-dsa), SLH-DSA (slh-dsa), ECDSA (p256, k256) or RSA (rsa).` }, { bucata: intreg }); } continue; } - const activ = activStatic(r, metoda, generic, ctx, m); - if (activ) adauga(r.crate, m.index, `${prefix}${tip}::${metoda}`, 'call', activ, intreg + (activ.bucataExtra || '')); + adauga(r.crate, m.index, `${prefix}${tip}::${metoda}`, 'call', activ, intreg + (activ.bucataExtra || '')); } // 2. aliasuri de tip cu parametru generic: type HmacSha256 = Hmac; type Kdf = Hkdf; @@ -230,11 +246,13 @@ export function detecteazaRust(ctx) { // rustls::crypto::ring::default_provider(), libsignal_protocol::process_prekey_bundle(...) for (const m of ctx.potriviri(/(?)?\s*\(/g)) { const seg = m[1].split('::').filter(Boolean); + if (RADACINI_PROPRII.has(seg[0])) continue; const u = locale.get(seg[0]); - const cale = u ? [...u.cale, ...seg.slice(1), m[2]] : [...seg, m[2]]; - if (RADACINI_PROPRII.has(cale[0]) || !CRATE[cale[0]]) continue; - const activ = activFunctie(cale, m[3], ctx, m); - if (activ) adauga(cale[0], m.index, cale.join('::'), 'call', activ, m[0].trim() + (activ.bucataExtra || '')); + const cai = u ? (CRATE[u.crate] ? [[...u.cale, ...seg.slice(1), m[2]]] : []) : caiPentru(seg).map((c) => [...c, m[2]]); + for (const cale of cai) { + const activ = activFunctie(cale, m[3], ctx, m); + if (activ) { adauga(cale[0], m.index, cale.join('::'), 'call', activ, m[0].trim() + (activ.bucataExtra || '')); break; } + } } // functii importate direct: use pbkdf2::pbkdf2_hmac; use blake3::hash; use libsignal_protocol::process_prekey_bundle; for (const [local, u] of locale) { @@ -251,10 +269,11 @@ export function detecteazaRust(ctx) { const seg = m[1].split('::').filter(Boolean); if (RADACINI_PROPRII.has(seg[0])) continue; const u = locale.get(seg[0]); - const cale = u ? [...u.cale, ...seg.slice(1)] : seg; - if (!CRATE[cale[0]]) continue; - const activ = activConstanta(cale, m[2], ctx, m); - if (activ) adauga(cale[0], m.index, `${cale.join('::')}::${m[2]}`, activ.fel || 'config', activ, m[0]); + const cai = u ? (CRATE[u.crate] ? [[...u.cale, ...seg.slice(1)]] : []) : caiPentru(seg); + for (const cale of cai) { + const activ = activConstanta(cale, m[2], ctx, m); + if (activ) { adauga(cale[0], m.index, `${cale.join('::')}::${m[2]}`, activ.fel || 'config', activ, m[0]); break; } + } } // variante importate direct (use jsonwebtoken::Algorithm; Algorithm::RS256) si tipuri parametru PQ scrise singure for (const m of ctx.potriviri(/(? probe.push({ nume, fn }); @@ -226,6 +226,15 @@ proba('rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey) asigura(a.length === 1 && a[0].classification === 'unknown' && /ed25519-dalek/.test(a[0].reason) && /ml-dsa/.test(a[0].reason), `fara use: ${JSON.stringify(a.map((x) => [x.name, x.classification]))}`); }); +proba('rust: un import glob (use crate::*) rezolva numele nerezolvate pe crate-ul lui, iar perechea KEM a lui libsignal nu e Curve25519', () => { + const g = gasiriDin(scan(FIX), 'rust/glob.rs'); + const k = g.find((x) => x.line === 10); + asigura(k && k.name === 'Kyber-1024' && /kem::KeyType::Kyber1024$/.test(k.api) && g.filter((x) => x.line === 10).length === 1, `kem::KeyPair::generate(KeyType::Kyber1024): ${JSON.stringify(g.filter((x) => x.line === 10).map((x) => [x.name, x.api]))}`); + const i = g.find((x) => x.line === 11); + asigura(i && i.name === 'X25519' && /IdentityKeyPair/.test(i.api), `IdentityKeyPair prin glob: ${JSON.stringify(i && [i.name, i.api])}`); + asigura(g.filter((x) => x.name === 'ML-KEM-1024').length === 2 && g.filter((x) => x.name === 'ML-DSA-65').length === 1, `libcrux: ${JSON.stringify(g.map((x) => x.line + ' ' + x.name))}`); +}); + proba('rust: importul fara apel e dovada slaba (unknown), importul cu apel in acelasi fisier nu se raporteaza', () => { const g = gasiriDin(scan(FIX), 'rust/cripto.rs'); const imp = g.filter((x) => x.evidenceKind === 'import');