From f2f6217b4ce3512d257b513e2f9afdbc83cabffd Mon Sep 17 00:00:00 2001 From: Aere Network Date: Sun, 18 Oct 2026 05:35:11 +0300 Subject: [PATCH] crypto-inventory 0.3.0: Rust detector (use-resolved names, static calls, generics, ring constants, Noise patterns), Cargo.toml manifests, BLAKE3; 44/44, negative control 21/21 --- CITATIONS-UNRESOLVED.md | 2 +- README.md | 6 +- control-plane/control-negativ-consola-web.mjs | 7 +- crypto-inventory/README.md | 16 +- crypto-inventory/lib/catalog.mjs | 2 + crypto-inventory/lib/detect-rust.mjs | 583 ++++++++++++++++++ crypto-inventory/lib/lexer.mjs | 68 ++ crypto-inventory/lib/manifeste.mjs | 71 ++- crypto-inventory/lib/scan.mjs | 8 +- crypto-inventory/test/control-negativ.mjs | 24 + crypto-inventory/test/echivalenta-0.1.0.mjs | 12 +- .../test/fixturi/manifeste/Cargo.toml | 21 + .../test/fixturi/negative/comentarii.rs | 23 + crypto-inventory/test/fixturi/rust/ambiguu.rs | 6 + crypto-inventory/test/fixturi/rust/cripto.rs | 53 ++ crypto-inventory/test/proba.mjs | 61 +- 16 files changed, 942 insertions(+), 21 deletions(-) create mode 100644 crypto-inventory/lib/detect-rust.mjs create mode 100644 crypto-inventory/test/fixturi/manifeste/Cargo.toml create mode 100644 crypto-inventory/test/fixturi/negative/comentarii.rs create mode 100644 crypto-inventory/test/fixturi/rust/ambiguu.rs create mode 100644 crypto-inventory/test/fixturi/rust/cripto.rs diff --git a/CITATIONS-UNRESOLVED.md b/CITATIONS-UNRESOLVED.md index 811e67a..cb85927 100644 --- a/CITATIONS-UNRESOLVED.md +++ b/CITATIONS-UNRESOLVED.md @@ -1,7 +1,7 @@ # Citations in this repository that you cannot open This file is generated by the publication stager of this repository (scripts/legaturi-declaratie.cjs in the Aere working -tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-14, of every path cited in this +tree) and enforced by legaturi.cjs. It is the complete list, measured on 2026-10-18, of every path cited in this repository that does not resolve to a published file. A citation is a promise that a claim is checkable. Where the target is not published, or names a file the reader creates diff --git a/README.md b/README.md index e1507c1..3e80626 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ notarization command of the verification layer and for the agents' x402 wallet ( | [`pq-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates | | [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 | | [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL | -| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, PEM blocks, dependency manifests): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow | +| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, Rust, PEM blocks, dependency manifests including Cargo.toml): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow | | [`verify-layer/`](verify-layer/) | an audit-log sidecar for any deployment: entries are AIP-23 envelopes in a hash chain, the runtime adapter records every running Docker or Kubernetes container without any secret value, and the head of the chain can be signed by the operator (ML-DSA-65) and notarized on Aere Network under a post-quantum validator certificate; it says plainly what that proves (the history before a published head) and what it does not (that the host told the truth); the same log as a Merkle tree (RFC 9162): signed tree heads, and inclusion and consistency proofs of a few hashes that anyone checks without the log | | [`proof-kinds/`](proof-kinds/) | the AIP-23 envelope builder the verification layer uses: fourteen proof kinds, one envelope format, digests instead of raw content | | [`readiness/`](readiness/) | the post-quantum readiness scanner of a public hostname: real TLS handshakes (hybrid only, hybrid preferred, TLS 1.2), HSTS, the certificate; no connection to an address not proven public; a rate limit per client that X-Forwarded-For cannot bypass | @@ -39,7 +39,7 @@ 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 | 37/37 (`node test/proba.mjs`); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 18/18 (`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 | 44/44 (`node test/proba.mjs`, measured 2026-10-18 with the Rust detector); 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) | | 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) | @@ -62,4 +62,4 @@ interface, command line and data (`definePolicy`, `verifyLedger`, `approve`, ... ## Licence -MIT, see [LICENSE](LICENSE). Files: 700 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42, 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: 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). diff --git a/control-plane/control-negativ-consola-web.mjs b/control-plane/control-negativ-consola-web.mjs index 00dcdfd..21034ea 100644 --- a/control-plane/control-negativ-consola-web.mjs +++ b/control-plane/control-negativ-consola-web.mjs @@ -29,12 +29,15 @@ function ruleaza(pagina) { const T = fs.mkdtempSync(path.join(os.tmpdir(), 'cons-web-ctl-')); let rele = 0; try { - const m0 = path.join(T, 'martor.html'); fs.copyFileSync(PAGINA, m0); + // (2026-10-16) capetele de rand se aduc la LF inainte de orice: copia de pe disc a unui checkout Windows (core.autocrlf) are CRLF, + // iar doua tipare cer '\n'; asa au iesit STRICAT ("apare de 0 ori") si poarta D-459 a stat NEMASURAT. Martorul e scris din ACEEASI + // forma ca plantarile, deci o plantare difera de martor numai prin defectul ei. + const src = fs.readFileSync(PAGINA, 'utf8').replace(/\r\n/g, '\n'); + const m0 = path.join(T, 'martor.html'); fs.writeFileSync(m0, src); const m = await ruleaza(m0); if (m.nemasurat) { console.log('NEMASURAT: proba nu poate rula aici (fara browser)'); process.exit(2); } if (m.rulat && m.cod === 0 && !m.rosii.length) console.log(' OK martorul: copia neatinsa verde'); else { rele++; console.log(` STRICAT martorul nu e verde (cod ${m.cod}, ${m.rosii.length} rosii)`); } - const src = fs.readFileSync(PAGINA, 'utf8'); const planteaza = async ([nume, din, inl, tinta], i) => { if (src.split(din).length !== 2) return [false, ` STRICAT ${nume}: tiparul apare de ${src.split(din).length - 1} ori`]; const f = path.join(T, `p${i}.html`); fs.writeFileSync(f, src.replace(din, inl)); diff --git a/crypto-inventory/README.md b/crypto-inventory/README.md index a8790ee..73c7c71 100644 --- a/crypto-inventory/README.md +++ b/crypto-inventory/README.md @@ -23,13 +23,23 @@ 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` | | 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`: cryptographic libraries that are declared | +| Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `go.mod`, `Cargo.toml`: cryptographic libraries that are declared | In Go an unused import does not compile, so an import of a crypto package is evidence of use; the import-level finding is dropped when the same file has a call-level finding that covers it. A blank import (`_ "crypto/sha512"`) is reported with a note that it only registers an implementation. +In Rust (added in 0.3.0, 2026-10-18, after the first run on a tree of 1,320 Rust files returned zero Rust +findings) the `use` declarations decide what a name means: `SigningKey` is Ed25519 when it comes from +`ed25519-dalek`, ML-DSA from `ml-dsa`, SLH-DSA from `slh-dsa`, ECDSA from `p256`/`k256`, RSA from `rsa`. +A name like that with no `use` naming its crate is reported as `unknown` with the candidates listed, not +guessed. An unused import is only a warning in Rust, so an imported crypto crate is reported as an +`import` finding only when the file has no recognized call from it. Not recognized: methods on values +(`.sign()`, `.encrypt()`) without their constructor in the same file, macros, and a project's own wrapper +types (the file that calls the crate directly is the one reported). + When the algorithm is given by a variable, the tool resolves it only when the file assigns that name exactly once, to a plain string literal, and the name is not a function parameter or loop variable (template literals whose `${...}` parts all resolve this way are also resolved). The finding then @@ -176,8 +186,8 @@ console.log(renderSummary(result)); configuration files at run time. Those appear as `unknown` at best, or not at all. - It does not prove that a use is exploitable, reachable or security-relevant. MD5 used as a cache key is reported like MD5 used for signatures; the reason text tells you what to check. -- It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java and Go. - Kotlin, Scala, C#, Rust, C and C++ files are only checked for PEM blocks. Files that are not UTF-8 +- It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java, Go and Rust. + Kotlin, Scala, C#, C and C++ files are only checked for PEM blocks. Files that are not UTF-8 text (including UTF-16) are treated as binary and counted as not read. - It does not execute anything from the scanned tree, and it makes no network requests. diff --git a/crypto-inventory/lib/catalog.mjs b/crypto-inventory/lib/catalog.mjs index 314192a..508a251 100644 --- a/crypto-inventory/lib/catalog.mjs +++ b/crypto-inventory/lib/catalog.mjs @@ -79,6 +79,7 @@ const HASH = { SHAKE256: { n: 'SHAKE256', c: CLS.S, xof: true }, BLAKE2B: { n: 'BLAKE2b', c: CLS.S }, BLAKE2S: { n: 'BLAKE2s', c: CLS.S }, BLAKE2B512: { n: 'BLAKE2b-512', c: CLS.S }, BLAKE2S256: { n: 'BLAKE2s-256', c: CLS.S }, + BLAKE3: { n: 'BLAKE3', c: CLS.S, q: 2, cl: 128 }, SM3: { n: 'SM3', c: CLS.S }, KECCAK256: { n: 'Keccak-256', c: CLS.S }, }; @@ -97,6 +98,7 @@ export function hashCanonic(s) { if (/^BLAKE2B-?512$/.test(t)) return 'BLAKE2B512'; if (/^BLAKE2S-?256$/.test(t)) return 'BLAKE2S256'; if (/^BLAKE2[BS]$/.test(t)) return t; + if (/^BLAKE3$/.test(t)) return 'BLAKE3'; if (/^SM3$/.test(t)) return 'SM3'; if (/^KECCAK-?256$/.test(t)) return 'KECCAK256'; return null; diff --git a/crypto-inventory/lib/detect-rust.mjs b/crypto-inventory/lib/detect-rust.mjs new file mode 100644 index 0000000..f9cef6e --- /dev/null +++ b/crypto-inventory/lib/detect-rust.mjs @@ -0,0 +1,583 @@ +// Detectorul Rust (0.3.0, 2026-10-18; scris dupa prima rulare a inventarului pe un arbore strain, cu 1.320 de fisiere Rust si +// zero constatari). Ce se recunoaste: +// - declaratiile `use` si `extern crate`: ele spun din ce crate vine fiecare nume din scop. Acelasi nume inseamna lucruri +// diferite (SigningKey e Ed25519 in ed25519-dalek, ML-DSA in ml-dsa, ECDSA in p256/k256, RSA in rsa), deci un nume +// ambiguu fara `use` nu se ghiceste: iese unknown, cu motivul scris; +// - apelurile statice Tip::metoda (Sha256::new, SigningKey::generate, Aes256Gcm::new_from_slice, RsaPrivateKey::new(rng, 2048)), +// parametrii generici (Hmac::, Hkdf::, type HmacSha256 = Hmac), functiile pe cale (blake3::hash, +// pbkdf2::pbkdf2_hmac::, pqcrypto_kyber::kyber1024::keypair), constantele lui ring (aead::AES_256_GCM), +// tiparele Noise ale lui snow, furnizorul criptografic al lui rustls, variantele Algorithm ale lui jsonwebtoken si oqs, +// parametrii ML-DSA / SLH-DSA / ML-KEM (MlDsa65, SlhDsaSha2_128s, MlKem768) si prekey-urile Kyber ale lui libsignal; +// - in Rust un import nefolosit e un avertisment, nu o eroare, deci importul e dovada SLABA: un crate criptografic importat se +// raporteaza ca 'import' (unknown) numai cand fisierul nu are niciun apel recunoscut din el. +// Nu se recunosc: metodele pe valori (.sign(), .update(), .encrypt()) fara constructorul lor in acelasi fisier, macro-urile, +// tipurile proprii care invelesc un crate (acolo se raporteaza fisierul care il cheama direct). +import { argumente, escapeRe } from './context.mjs'; +import { jws } from './catalog.mjs'; + +const RADACINI_PROPRII = new Set(['crate', 'self', 'super', 'std', 'core', 'alloc']); + +// crate-urile criptografice cunoscute (numele din `use`, cu liniuta jos) si grupurile pe care le pot produce (suprimarea importului) +const CRATE = { + sha2: { ofera: 'SHA-224/256/384/512', grupuri: ['HASH', 'MAC', 'KDF'] }, + sha1: { ofera: 'SHA-1', grupuri: ['HASH', 'MAC', 'KDF'] }, + md5: { ofera: 'MD5', grupuri: ['HASH'] }, + sha3: { ofera: 'SHA3, Keccak, SHAKE', grupuri: ['HASH'] }, + blake2: { ofera: 'BLAKE2b/BLAKE2s', grupuri: ['HASH', 'MAC'] }, + blake3: { ofera: 'BLAKE3', grupuri: ['HASH'] }, + hmac: { ofera: 'HMAC', grupuri: ['MAC'] }, + hkdf: { ofera: 'HKDF', grupuri: ['KDF'] }, + argon2: { ofera: 'Argon2', grupuri: ['KDF'] }, + pbkdf2: { ofera: 'PBKDF2', grupuri: ['KDF'] }, + scrypt: { ofera: 'scrypt', grupuri: ['KDF'] }, + bcrypt: { ofera: 'bcrypt', grupuri: ['KDF'] }, + ed25519_dalek: { ofera: 'Ed25519 signatures', grupuri: ['EDDSA'] }, + x25519_dalek: { ofera: 'X25519 key agreement', grupuri: ['XDH'] }, + curve25519_dalek: { ofera: 'Curve25519 group arithmetic (Ristretto, Edwards, Montgomery)', grupuri: ['EC', 'XDH', 'EDDSA'] }, + chacha20poly1305: { ofera: 'ChaCha20-Poly1305 and XChaCha20-Poly1305', grupuri: ['CIPHER'] }, + aes_gcm: { ofera: 'AES-GCM', grupuri: ['CIPHER'] }, + aes_gcm_siv: { ofera: 'AES-GCM-SIV', grupuri: ['CIPHER'] }, + aes_siv: { ofera: 'AES-SIV', grupuri: ['CIPHER'] }, + aes: { ofera: 'the AES block cipher (mode chosen by the caller)', grupuri: ['CIPHER'] }, + cbc: { ofera: 'the CBC mode', grupuri: ['CIPHER'] }, + ctr: { ofera: 'the CTR mode', grupuri: ['CIPHER'] }, + ecb: { ofera: 'the ECB mode', grupuri: ['CIPHER'] }, + rsa: { ofera: 'RSA', grupuri: ['RSA'] }, + p256: { ofera: 'NIST P-256 (ECDSA, ECDH)', grupuri: ['EC', 'ECDSA', 'ECDH'] }, + p384: { ofera: 'NIST P-384 (ECDSA, ECDH)', grupuri: ['EC', 'ECDSA', 'ECDH'] }, + p521: { ofera: 'NIST P-521 (ECDSA, ECDH)', grupuri: ['EC', 'ECDSA', 'ECDH'] }, + k256: { ofera: 'secp256k1 (ECDSA, ECDH)', grupuri: ['SECP256K1', 'ECDH'] }, + secp256k1: { ofera: 'secp256k1 (ECDSA, Schnorr)', grupuri: ['SECP256K1'] }, + ml_kem: { ofera: 'ML-KEM (FIPS 203)', grupuri: ['MLKEM'] }, + ml_dsa: { ofera: 'ML-DSA (FIPS 204)', grupuri: ['MLDSA'] }, + slh_dsa: { ofera: 'SLH-DSA (FIPS 205)', grupuri: ['SLHDSA'] }, + pqcrypto_kyber: { ofera: 'Kyber (pre-standard ML-KEM)', grupuri: ['PREPQ'] }, + pqcrypto_mlkem: { ofera: 'ML-KEM (PQClean)', grupuri: ['MLKEM'] }, + pqcrypto_dilithium: { ofera: 'Dilithium (pre-standard ML-DSA)', grupuri: ['PREPQ'] }, + pqcrypto_mldsa: { ofera: 'ML-DSA (PQClean)', grupuri: ['MLDSA'] }, + pqcrypto_sphincsplus: { ofera: 'SPHINCS+ (pre-standard SLH-DSA)', grupuri: ['PREPQ'] }, + pqcrypto_falcon: { ofera: 'Falcon', grupuri: ['FALCON'] }, + 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'] }, + 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'] }, + 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'] }, + native_tls: { ofera: 'the platform TLS stack', grupuri: ['TLS'] }, + snow: { ofera: 'the Noise protocol framework', grupuri: ['XDH', 'CIPHER', 'HASH'] }, + jsonwebtoken: { ofera: 'JWS HS*/RS*/PS*/ES*/EdDSA', grupuri: ['JWT-HMAC', 'RSA', 'ECDSA', 'EDDSA', 'JWT-NONE'] }, + age: { ofera: 'age file encryption: X25519 recipients, scrypt passphrases, ChaCha20-Poly1305', grupuri: ['XDH', 'KDF', 'CIPHER'] }, + crypto_box: { ofera: 'NaCl crypto_box: X25519 + XSalsa20/XChaCha20-Poly1305', grupuri: ['XDH', 'CIPHER'] }, + arti_client: { ofera: 'a Tor client (circuit cryptography negotiated with relays)', grupuri: ['UNKNOWN'] }, +}; +const NUME_CARGO = (c) => c.replace(/_/g, '-'); + +// tipuri care au un singur proprietar printre crate-urile cunoscute: se recunosc si fara `use` (de ex. dupa un `use sha2::*`) +const UNIC = {}; +for (const [crate, tipuri] of Object.entries({ + sha2: ['Sha224', 'Sha256', 'Sha384', 'Sha512', 'Sha512_224', 'Sha512_256'], + 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'], + chacha20poly1305: ['ChaCha20Poly1305', 'XChaCha20Poly1305', 'ChaCha8Poly1305', 'ChaCha12Poly1305', 'XChaCha8Poly1305', 'XChaCha12Poly1305'], + aes_gcm: ['Aes128Gcm', 'Aes256Gcm'], aes_gcm_siv: ['Aes128GcmSiv', 'Aes256GcmSiv'], aes_siv: ['Aes128SivAead', 'Aes256SivAead'], + aes: ['Aes128', 'Aes192', 'Aes256'], + rsa: ['RsaPrivateKey', 'RsaPublicKey', 'Pkcs1v15Sign', 'Pkcs1v15Encrypt', 'Oaep', 'Pss'], + x25519_dalek: ['StaticSecret', 'EphemeralSecret', 'ReusableSecret'], + curve25519_dalek: ['RistrettoPoint', 'EdwardsPoint', 'MontgomeryPoint'], + ml_kem: ['MlKem512', 'MlKem768', 'MlKem1024'], ml_dsa: ['MlDsa44', 'MlDsa65', 'MlDsa87'], + libsignal_protocol: ['PreKeyBundle', 'IdentityKeyPair', 'SignalProtocolAddress', 'KyberPreKeyRecord'], + arti_client: ['TorClient'], +})) for (const t of tipuri) UNIC[t] = crate; + +const HASH_TIP = { + Sha224: 'SHA224', Sha256: 'SHA256', Sha384: 'SHA384', Sha512: 'SHA512', Sha512_224: 'SHA512/224', Sha512_256: 'SHA512/256', + Sha1: 'SHA1', Md5: 'MD5', + Sha3_224: 'SHA3-224', Sha3_256: 'SHA3-256', Sha3_384: 'SHA3-384', Sha3_512: 'SHA3-512', Keccak256: 'KECCAK256', Shake128: 'SHAKE128', Shake256: 'SHAKE256', + Blake2b512: 'BLAKE2B512', Blake2s256: 'BLAKE2S256', Blake2b: 'BLAKE2B', Blake2s: 'BLAKE2S', +}; +const hashDinTip = (t) => HASH_TIP[String(t || '').split('::').pop().trim()] || null; +// crate-ul care detine tipul de hash dintr-un parametru generic (sha2::Sha256 -> sha2), ca `use sha2` sa conteze ca folosit +const crateHashDinTip = (t) => { const n = String(t || '').split('::').pop().trim(); return UNIC[n] || null; }; +const CTOR = /^(new|default|new_from_slice|new_varkey|new_with_prefix|digest|digest_variable|new_variable|generate|generate_key|generate_nonce|random|random_from_rng|from|from_bytes|from_slice|from_keypair_bytes|from_pkcs8_der|from_pkcs8_pem|from_pkcs1_der|from_pkcs1_pem|from_public_key_der|from_public_key_pem|from_components|try_from|new_with_secret|from_seed|key_gen|keypair|try_sign|sign|verify)$/; + +// --- `use` --------------------------------------------------------------------------------------------------------------------- +export function useuriRust(ctx) { + const r = []; // { crate, cale: [segmente], local, glob, pos } + for (const m of ctx.potriviri(/(? r.push({ crate: cale[0], cale, local, glob, pos: m.index, sf: m.index + m[0].length })); + } + for (const m of ctx.potriviri(/(? u.cale.length && !RADACINI_PROPRII.has(u.crate)); +} + +function dezvolta(text, prefix, emite) { + let t = text.trim(); + if (t.startsWith('::')) t = t.slice(2); + const k = t.indexOf('{'); + if (k >= 0) { + const baza = t.slice(0, k).replace(/::\s*$/, '').trim(); + const inauntru = t.slice(k + 1, t.lastIndexOf('}')); + const pre = baza ? [...prefix, ...baza.split('::').map((s) => s.trim()).filter(Boolean)] : prefix; + for (const parte of imparteVirgule(inauntru)) if (parte.trim()) dezvolta(parte, pre, emite); + return; + } + const alias = /^(.*?)\s+as\s+([A-Za-z_]\w*|_)$/.exec(t); + const seg = (alias ? alias[1] : t).trim().split('::').map((s) => s.trim()).filter(Boolean); + if (!seg.length) return; + const cale = [...prefix, ...seg]; + if (cale[cale.length - 1] === '*') { if (cale.length > 1) emite(cale.slice(0, -1), null, true); return; } + if (cale[cale.length - 1] === 'self') cale.pop(); + if (!cale.length) return; + emite(cale, alias ? alias[2] : cale[cale.length - 1], false); +} + +function imparteVirgule(s) { + const r = []; + let ad = 0; + let inc = 0; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === '{') ad++; + else if (c === '}') ad--; + else if (c === ',' && ad === 0) { r.push(s.slice(inc, i)); inc = i + 1; } + } + r.push(s.slice(inc)); + return r; +} + +// --- detectorul ----------------------------------------------------------------------------------------------------------------- +export function detecteazaRust(ctx) { + const useuri = useuriRust(ctx); + const locale = new Map(); // numele din scop -> { crate, cale } + const globuri = new Set(); // crate-uri aduse cu `use crate::*` (numele lor UNIC se recunosc) + for (const u of useuri) { + if (u.glob) { globuri.add(u.crate); continue; } + if (u.local && u.local !== '_' && !locale.has(u.local)) locale.set(u.local, { crate: u.crate, cale: u.cale }); + } + // numele aduse din propriul crate (use crate::chei::KeyPair, use super::*) nu sunt ambigue intre crate-urile criptografice + const proprii = new Set(); + for (const m of ctx.potriviri(/(? { if (local) proprii.add(local); }); + const folosite = new Set(); // crate-urile cu cel putin un apel recunoscut in acest fisier + // o potrivire din interiorul unei declaratii `use` nu e o folosire (MlDsa65 din `use ml_dsa::MlDsa65` nu e un apel) + const inUse = (pos) => useuri.some((u) => pos >= u.pos && pos < u.sf); + const potriviri = (re) => ctx.potriviri(re).filter((m) => !inUse(m.index)); + // parametrul PQ al unui rand se raporteaza o singura data, oricate forme il poarta (SigningKey::::new si MlDsa65 din generic) + const pqPeRand = new Set(); + const adauga = (crate, pos, api, fel, activ, bucata) => { + if (/^(MLDSA|MLKEM|SLHDSA)$/.test(activ.grup)) { + const k = `${ctx.poz(pos).line}|${activ.grup}|${activ.param || ''}`; + if (pqPeRand.has(k)) return; + pqPeRand.add(k); + } + if (crate) folosite.add(crate); + if (activ.hash && activ.crateHash) folosite.add(activ.crateHash); + const curat = { ...activ }; + delete curat.crateHash; + delete curat.bucataExtra; + 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) => { + const seg = prefix ? prefix.split('::').filter(Boolean) : []; + if (seg.length) { + if (RADACINI_PROPRII.has(seg[0])) return null; + 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; + } + 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; + }; + + // 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); + if (!r) { + if (!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 || '')); + } + + // 2. aliasuri de tip cu parametru generic: type HmacSha256 = Hmac; type Kdf = Hkdf; + for (const m of ctx.potriviri(/(?;]+)>/g)) { + const r = rezolvaCale(m[1], m[2]); + if (!r) continue; + const h = hashDinTip(m[3]); + const activ = m[2] === 'Hkdf' ? { grup: 'KDF', nume: 'HKDF', hash: h || undefined, functii: ['derive'], crateHash: crateHashDinTip(m[3]) } + : (h ? { grup: 'MAC', hash: h, functii: ['tag'], crateHash: crateHashDinTip(m[3]) } : { grup: 'UNKNOWN', motiv: `HMAC over "${m[3].trim()}", a hash this tool does not resolve.` }); + adauga(r.crate, m.index, `type = ${m[2]}<${m[3].trim()}>`, 'config', activ, m[0]); + } + + // 3. functii pe cale: blake3::hash(...), pbkdf2::pbkdf2_hmac::(...), pqcrypto_kyber::kyber1024::keypair(), scrypt::scrypt(...), + // 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); + 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 || '')); + } + // functii importate direct: use pbkdf2::pbkdf2_hmac; use blake3::hash; use libsignal_protocol::process_prekey_bundle; + for (const [local, u] of locale) { + if (!/^[a-z_]\w*$/.test(local) || !CRATE[u.crate] || u.cale.length < 2) continue; + for (const m of ctx.potriviri(new RegExp(`(?)?\\s*\\(`, 'g'))) { + 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 || '')); + } + } + + // 4. constantele lui ring (aead::AES_256_GCM, signature::ED25519) si variantele enum (Algorithm::RS256, KeyType::Kyber1024, + // SslVersion::TLS1_2), rezolvate prin `use` + for (const m of ctx.potriviri(/(? u.crate === 'slh_dsa') || globuri.has('slh_dsa')) { + for (const m of potriviri(/(?, ecb::Decryptor, ctr::Ctr128BE + for (const m of ctx.potriviri(/(?`, 'call', { grup: 'CIPHER', nume: 'AES', param: m[3], mod, functii: [m[2] === 'Decryptor' ? 'decrypt' : 'encrypt'] }, m[0]); + } + + // 6. tiparul Noise al lui snow, scris ca sir: Noise_XX_25519_ChaChaPoly_SHA256 + if ([...locale.values()].some((u) => u.crate === 'snow') || /(? u.crate === 'secp256k1')) { + for (const m of ctx.potriviri(/\.\s*(sign_ecdsa|sign_ecdsa_recoverable|sign_schnorr|verify_ecdsa|verify_schnorr)\s*\(/g)) { + const schnorr = /schnorr/.test(m[1]); + adauga('secp256k1', m.index, `secp256k1 .${m[1]}`, 'call', { grup: 'SECP256K1', nume: schnorr ? 'Schnorr-secp256k1' : undefined, functii: [/^sign/.test(m[1]) ? 'sign' : 'verify'] }, m[0]); + } + } + + // 8. openssl: versiunea minima/maxima TLS + if ([...locale.values()].some((u) => u.crate === 'openssl') || /(? [k]; + const rol = /^(generate|random|random_from_rng|key_gen|keypair|new|default|from_seed)$/.test(metoda) ? 'keygen' : /^(try_sign|sign)$/.test(metoda) ? 'sign' : /^verify/.test(metoda) ? 'verify' : 'other'; + switch (crate) { + case 'sha2': case 'sha1': case 'md5': case 'sha3': case 'blake2': { + const h = HASH_TIP[tip]; + if (!h || !/^(new|default|digest|new_with_prefix|digest_variable|new_variable|new_keyed)$/.test(metoda)) return null; + if (/Mac$/.test(tip)) return { grup: 'MAC', hash: h, functii: ['tag'] }; + return { grup: 'HASH', hash: h, functii: ['digest'] }; + } + 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; + 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.' }; + } + case 'hkdf': { + if (tip !== 'Hkdf' || !/^(new|extract|from_prk)$/.test(metoda)) return null; + return { grup: 'KDF', nume: 'HKDF', hash: hashDinTip(generic) || undefined, functii: ['derive'], crateHash: crateHashDinTip(generic) }; + } + case 'argon2': { + if (tip !== 'Argon2' || !/^(new|default|new_with_secret)$/.test(metoda)) return null; + const varianta = /Argon2(id|i|d)\b/.exec(ctx.code.slice(m.index, m.index + m[0].length + 200) || ''); + return { grup: 'KDF', nume: varianta ? `Argon2${varianta[1]}` : 'Argon2', functii: ['derive'], bucataExtra: varianta ? ` ${varianta[0]}` : '' }; + } + case 'ed25519_dalek': + if (/^(SigningKey|Keypair|ExpandedSecretKey)$/.test(tip)) return { grup: 'EDDSA', nume: 'Ed25519', functii: fn(rol === 'other' ? 'keygen' : rol) }; + if (tip === 'VerifyingKey' || tip === 'PublicKey') return { grup: 'EDDSA', nume: 'Ed25519', functii: ['verify'] }; + return null; + case 'x25519_dalek': + if (/^(StaticSecret|EphemeralSecret|ReusableSecret)$/.test(tip)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }; + if (tip === 'PublicKey') return { grup: 'XDH', nume: 'X25519', functii: ['other'] }; + return null; + case 'curve25519_dalek': + if (/^(RistrettoPoint|EdwardsPoint|MontgomeryPoint|Scalar)$/.test(tip)) return { grup: 'EC', curba: 'Curve25519', nume: 'Curve25519 group arithmetic', primitiv: 'other', nota: 'curve25519-dalek: Ristretto/Edwards/Montgomery group operations; the protocol built on them (signatures, key agreement, VRF) decides the primitive.' }; + return null; + case 'chacha20poly1305': { + const n = { ChaCha20Poly1305: 'ChaCha20-Poly1305', XChaCha20Poly1305: 'XChaCha20-Poly1305', ChaCha8Poly1305: 'ChaCha8-Poly1305', ChaCha12Poly1305: 'ChaCha12-Poly1305', XChaCha8Poly1305: 'XChaCha8-Poly1305', XChaCha12Poly1305: 'XChaCha12-Poly1305' }[tip]; + return n && /^(new|new_from_slice|generate_key|generate_nonce)$/.test(metoda) ? { grup: 'CIPHER', nume: n, functii: ['encrypt'] } : null; + } + case 'aes_gcm': case 'aes_gcm_siv': case 'aes_siv': case 'aes': { + const k = /^Aes(128|192|256)(Gcm|GcmSiv|SivAead)?$/.exec(tip); + if (!k || !/^(new|new_from_slice|generate_key|generate_nonce)$/.test(metoda)) return null; + const mod = k[2] === 'Gcm' ? 'gcm' : k[2] === 'GcmSiv' ? 'gcm-siv' : k[2] === 'SivAead' ? 'siv' : undefined; + return { grup: 'CIPHER', nume: 'AES', param: k[1], mod, functii: ['encrypt'], nota: mod ? undefined : 'Raw block cipher (one block at a time); the mode of operation is chosen by the caller.' }; + } + case 'rsa': { + if (tip === 'RsaPrivateKey') { + let bits; + if (metoda === 'new' || metoda === 'new_with_exp') { + const parti = argsDupa(ctx, m).split(','); + const b = parti[1] && /^\s*(\d{3,5})\s*$/.exec(parti[1]); + if (b) bits = b[1]; + } + return { grup: 'RSA', param: bits, functii: fn(rol === 'other' ? 'other' : rol), bucataExtra: bits ? ` ${bits}` : '' }; + } + if (tip === 'RsaPublicKey') return { grup: 'RSA', functii: ['other'] }; + if (tip === 'Pkcs1v15Sign') return { grup: 'RSA', primitiv: 'signature', padding: 'pkcs1v15', hash: hashDinTip(generic) || undefined, functii: ['sign', 'verify'] }; + if (tip === 'Pss') return { grup: 'RSA', primitiv: 'signature', padding: 'pss', hash: hashDinTip(generic) || undefined, functii: ['sign', 'verify'] }; + if (tip === 'Oaep') return { grup: 'RSA', primitiv: 'pke', padding: 'oaep', hash: hashDinTip(generic) || undefined, functii: ['encrypt'] }; + if (tip === 'Pkcs1v15Encrypt') return { grup: 'RSA', primitiv: 'pke', padding: 'pkcs1v15', functii: ['encrypt'] }; + return null; + } + case 'p256': case 'p384': case 'p521': case 'k256': { + const curba = { p256: 'secp256r1', p384: 'secp384r1', p521: 'secp521r1', k256: 'secp256k1' }[crate]; + const g = crate === 'k256' ? 'SECP256K1' : 'ECDSA'; + if (cale.includes('ecdh') || /^(EphemeralSecret)$/.test(tip)) return { grup: 'ECDH', curba, functii: ['keygen'] }; + if (cale.includes('ecdsa') || /^(SigningKey|VerifyingKey|Signature)$/.test(tip)) return { grup: g, curba, functii: fn(rol === 'other' ? (tip === 'VerifyingKey' ? 'verify' : 'keygen') : rol) }; + if (/^(SecretKey|PublicKey|NonZeroScalar|AffinePoint|ProjectivePoint)$/.test(tip)) return { grup: crate === 'k256' ? 'SECP256K1' : 'EC', curba, functii: ['keygen'] }; + return null; + } + case 'secp256k1': + if (/^(Secp256k1|SecretKey|Keypair|KeyPair|PublicKey|XOnlyPublicKey)$/.test(tip)) return { grup: 'SECP256K1', functii: ['keygen'] }; + return null; + case 'ml_kem': { + const k = /^MlKem(512|768|1024)$/.exec(tip); + return k ? { grup: 'MLKEM', param: k[1], functii: fn(/^generate/.test(metoda) ? 'keygen' : /encaps/.test(metoda) ? 'encapsulate' : /decaps/.test(metoda) ? 'decapsulate' : 'other') } : null; + } + case 'ml_dsa': { + const k = /^MlDsa(44|65|87)$/.exec(tip); + if (k) return { grup: 'MLDSA', param: k[1], functii: fn(rol) }; + const g = /MlDsa(44|65|87)/.exec(generic || ''); + if (/^(SigningKey|VerifyingKey|KeyPair|Signature)$/.test(tip)) return { grup: 'MLDSA', param: g ? g[1] : undefined, functii: fn(rol === 'other' ? (tip === 'VerifyingKey' ? 'verify' : 'keygen') : rol) }; + return null; + } + case 'slh_dsa': { + const k = /^(?:SlhDsa)?(Sha2|Shake)_(128|192|256)([sf])$/.exec(tip); + if (k) return { grup: 'SLHDSA', param: `${k[1].toUpperCase()}-${k[2]}${k[3]}`, functii: fn(rol) }; + const g = /(?:SlhDsa)?(Sha2|Shake)_(128|192|256)([sf])/.exec(generic || ''); + if (/^(SigningKey|VerifyingKey|Signature)$/.test(tip)) return { grup: 'SLHDSA', param: g ? `${g[1].toUpperCase()}-${g[2]}${g[3]}` : undefined, functii: fn(rol === 'other' ? (tip === 'VerifyingKey' ? 'verify' : 'keygen') : rol) }; + return null; + } + case 'libsignal_protocol': + if (tip === 'PreKeyBundle' && metoda === 'new') return { 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.' }; + if (/^(IdentityKeyPair|KeyPair|PrivateKey)$/.test(tip) && /^(generate|new|from_private_key_bytes|deserialize)$/.test(metoda)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: 'libsignal identity and prekey keys are Curve25519 (signatures over them are XEdDSA).' }; + if (tip === 'KyberPreKeyRecord' || tip === 'KeyType') return null; + return null; + case 'ring': + if (tip === 'Ed25519KeyPair') return { grup: 'EDDSA', nume: 'Ed25519', functii: fn(/generate/.test(metoda) ? 'keygen' : 'other') }; + if (tip === 'RsaKeyPair') return { grup: 'RSA', primitiv: 'signature', functii: ['sign'] }; + 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': { + 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}` }; + return null; + } + case 'rustls': case 'tokio_rustls': case 'hyper_rustls': case 'native_tls': + if (/^(ClientConfig|ServerConfig)$/.test(tip) && /^(builder|builder_with_provider|builder_with_protocol_versions)$/.test(metoda)) return { grup: 'TLS', param: 'negotiated', nume: `rustls ${tip}` }; + if (/^(TlsConnector|TlsAcceptor|HttpsConnectorBuilder)$/.test(tip)) return { grup: 'TLS', param: 'negotiated', nume: `${NUME_CARGO(crate)} ${tip}` }; + return null; + case 'pqcrypto_kyber': case 'pqcrypto_mlkem': case 'pqcrypto_dilithium': case 'pqcrypto_mldsa': case 'pqcrypto_sphincsplus': case 'pqcrypto_falcon': case 'pqcrypto_hqc': case 'pqcrypto_classicmceliece': + return null; // functiile lor sunt pe cale (pasul 3) + case 'oqs': + return null; // variantele Algorithm (pasul 4) + case 'age': + if (cale.includes('x25519') && /^(Identity|Recipient)$/.test(tip)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: 'age: X25519 recipients; the file key is wrapped and the payload encrypted with ChaCha20-Poly1305.' }; + if (cale.includes('scrypt') && /^(Identity|Recipient)$/.test(tip)) return { grup: 'KDF', nume: 'scrypt', functii: ['derive'] }; + return null; + case 'crypto_box': + if (/^(SalsaBox|ChaChaBox)$/.test(tip) && metoda === 'new') return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: `crypto_box: X25519 key agreement with ${tip === 'SalsaBox' ? 'XSalsa20-Poly1305' : 'XChaCha20-Poly1305'}.` }; + if (tip === 'SecretKey' && /^(generate|from_bytes)$/.test(metoda)) return { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }; + return null; + case 'arti_client': + 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 'jsonwebtoken': + return null; // variantele Algorithm (pasul 4) + default: + return null; + } +} + +// --- functii pe cale ------------------------------------------------------------------------------------------------------------ +function activFunctie(cale, generic, ctx, m) { + const crate = cale[0]; + const fnume = cale[cale.length - 1]; + const modul = cale.length >= 3 ? cale[cale.length - 2] : null; + switch (crate) { + case 'blake3': + return /^(hash|keyed_hash|derive_key)$/.test(fnume) ? { grup: 'HASH', hash: 'BLAKE3', functii: ['digest'] } : null; + case 'pbkdf2': { + if (!/^pbkdf2(_hmac|_hmac_array|_array|_check|_simple)?$/.test(fnume)) return null; + const h = hashDinTip(generic ? generic.replace(/^.*?<\s*/, '').replace(/\s*>.*$/, '') : ''); + return { grup: 'KDF', nume: 'PBKDF2', hash: h || undefined, functii: ['derive'], crateHash: crateHashDinTip(generic ? generic.replace(/^.*?<\s*/, '').replace(/\s*>.*$/, '') : '') }; + } + case 'scrypt': + return /^(scrypt|scrypt_simple|scrypt_check)$/.test(fnume) ? { grup: 'KDF', nume: 'scrypt', functii: ['derive'] } : null; + case 'bcrypt': + return /^(hash|hash_with_salt|verify|hash_with_result)$/.test(fnume) ? { grup: 'KDF', nume: 'bcrypt', functii: ['derive'] } : null; + case 'rustls': + if (fnume === 'default_provider' && modul === 'ring') return { grup: 'XDH', nume: 'X25519', functii: ['keygen'], nota: 'rustls with the ring provider: ring implements no ML-KEM, so the key-exchange groups are X25519, P-256 and P-384 only (no X25519MLKEM768).' }; + 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 'libsignal_protocol': + if (fnume === 'process_prekey_bundle') return { grup: 'LIB-MULTI', nume: 'libsignal session setup (X3DH/PQXDH)', motiv: 'libsignal session setup from a 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.' }; + return null; + case 'pqcrypto_kyber': case 'pqcrypto_mlkem': case 'pqcrypto_dilithium': case 'pqcrypto_mldsa': case 'pqcrypto_sphincsplus': case 'pqcrypto_falcon': case 'pqcrypto_hqc': case 'pqcrypto_classicmceliece': { + if (!/^(keypair|encapsulate|decapsulate|sign|open|detached_sign|verify_detached_signature)$/.test(fnume) || !modul) return null; + const f = /^(keypair)$/.test(fnume) ? 'keygen' : /encaps/.test(fnume) ? 'encapsulate' : /decaps/.test(fnume) ? 'decapsulate' : /sign$/.test(fnume) ? 'sign' : /open|verify/.test(fnume) ? 'verify' : 'other'; + let k; + if ((k = /^kyber(512|768|1024)(90s)?$/.exec(modul))) return { grup: 'PREPQ', nume: `Kyber-${k[1]}`, primitiv: 'kem', functii: [f] }; + if ((k = /^mlkem(512|768|1024)$/.exec(modul))) return { grup: 'MLKEM', param: k[1], functii: [f] }; + if ((k = /^dilithium(2|3|5)(aes)?$/.exec(modul))) return { grup: 'PREPQ', nume: `Dilithium${k[1]}`, primitiv: 'signature', functii: [f] }; + if ((k = /^mldsa(44|65|87)$/.exec(modul))) return { grup: 'MLDSA', param: k[1], functii: [f] }; + if (/^sphincs/.test(modul)) return { grup: 'PREPQ', nume: `SPHINCS+ (${modul})`, primitiv: 'signature', functii: [f] }; + if ((k = /^falcon(?:padded)?(512|1024)$/.exec(modul))) return { grup: 'FALCON', param: k[1], functii: [f] }; + if ((k = /^hqc(128|192|256)$/.exec(modul))) return { grup: 'HQC', nume: `HQC-${k[1]}`, functii: [f] }; + if (/^mceliece/.test(modul)) return { grup: 'UNKNOWN', nume: `Classic McEliece (${modul})`, motiv: 'Classic McEliece: code-based KEM, NIST round-4 candidate, not standardized.' }; + return null; + } + default: + return null; + } +} + +// --- constante si variante enum (ring, jsonwebtoken, oqs, libsignal kem::KeyType) ---------------------------------------------------- +function activConstanta(cale, nume, ctx, m) { + const crate = cale[0]; + const modul = cale[cale.length - 1]; + let k; + if (crate === 'ring') { + if (modul === 'signature') { + if (nume === 'ED25519') return { grup: 'EDDSA', nume: 'Ed25519', functii: ['verify'] }; + if ((k = /^ECDSA_P(256|384)_SHA(256|384)_(ASN1|FIXED)(_SIGNING)?$/.exec(nume))) return { grup: 'ECDSA', curba: `secp${k[1]}r1`, hash: `SHA${k[2]}`, functii: [k[4] ? 'sign' : 'verify'] }; + if ((k = /^RSA_(PKCS1|PSS)_(?:\d+_\d+_)?SHA(1|256|384|512)(_LEGACY_KEY)?$/.exec(nume))) return { grup: 'RSA', primitiv: 'signature', padding: k[1] === 'PSS' ? 'pss' : 'pkcs1v15', hash: `SHA${k[2]}`, functii: ['sign', 'verify'] }; + return null; + } + if (modul === 'agreement') { + if (nume === 'X25519') return { grup: 'XDH', nume: 'X25519', functii: ['keygen'] }; + if ((k = /^ECDH_P(256|384)$/.exec(nume))) return { grup: 'ECDH', curba: `secp${k[1]}r1`, functii: ['keygen'] }; + return null; + } + if (modul === 'digest') { + if ((k = /^SHA(1_FOR_LEGACY_USE_ONLY|256|384|512|512_256)$/.exec(nume))) return { grup: 'HASH', hash: k[1].startsWith('1') ? 'SHA1' : k[1] === '512_256' ? 'SHA512/256' : `SHA${k[1]}`, functii: ['digest'] }; + return null; + } + if (modul === 'aead') { + if ((k = /^AES_(128|256)_GCM$/.exec(nume))) return { grup: 'CIPHER', nume: 'AES', param: k[1], mod: 'gcm', functii: ['encrypt'] }; + if (nume === 'CHACHA20_POLY1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt'] }; + return null; + } + if (modul === 'hmac' && (k = /^HMAC_SHA(1_FOR_LEGACY_USE_ONLY|256|384|512)$/.exec(nume))) return { grup: 'MAC', hash: k[1].startsWith('1') ? 'SHA1' : `SHA${k[1]}`, functii: ['tag'] }; + if (modul === 'hkdf' && (k = /^HKDF_SHA(1_FOR_LEGACY_USE_ONLY|256|384|512)$/.exec(nume))) return { grup: 'KDF', nume: 'HKDF', hash: k[1].startsWith('1') ? 'SHA1' : `SHA${k[1]}`, functii: ['derive'] }; + if (modul === 'pbkdf2' && (k = /^PBKDF2_HMAC_SHA(1|256|384|512)$/.exec(nume))) return { grup: 'KDF', nume: 'PBKDF2', hash: `SHA${k[1]}`, functii: ['derive'] }; + return null; + } + if (crate === 'jsonwebtoken' && modul === 'Algorithm') { + return /^(HS256|HS384|HS512|RS256|RS384|RS512|PS256|PS384|PS512|ES256|ES384|EdDSA)$/.test(nume) ? { ...jws(nume), functii: ['sign', 'verify'] } : null; + } + if (crate === 'oqs' && modul === 'Algorithm') { + if ((k = /^MlKem(512|768|1024)$/.exec(nume))) return { grup: 'MLKEM', param: k[1] }; + if ((k = /^Kyber(512|768|1024)/.exec(nume))) return { grup: 'PREPQ', nume: `Kyber-${k[1]}`, primitiv: 'kem' }; + if ((k = /^MlDsa(44|65|87)$/.exec(nume))) return { grup: 'MLDSA', param: k[1] }; + if ((k = /^Dilithium(2|3|5)/.exec(nume))) return { grup: 'PREPQ', nume: `Dilithium${k[1]}`, primitiv: 'signature' }; + if ((k = /^Falcon(512|1024)$/.exec(nume))) return { grup: 'FALCON', param: k[1] }; + if (/^Sphincs/.test(nume)) return { grup: 'PREPQ', nume: `SPHINCS+ (${nume})`, primitiv: 'signature' }; + if ((k = /^Hqc(128|192|256)$/.exec(nume))) return { grup: 'HQC', nume: `HQC-${k[1]}` }; + if (/^(BikeL|ClassicMcEliece|FrodoKem|Ntru|Mayo|Cross|Uov|Snova)/.test(nume)) return { grup: 'UNKNOWN', nume: `liboqs ${nume}`, motiv: `${nume}: a post-quantum scheme that is not a NIST standard (liboqs experimental or round-4 algorithm).` }; + return null; + } + if (crate === 'libsignal_protocol' && modul === 'KeyType') { + if ((k = /^Kyber(768|1024)$/.exec(nume))) return { grup: 'PREPQ', nume: `Kyber-${k[1]}`, primitiv: 'kem', nota: 'Signal PQXDH prekeys: CRYSTALS-Kyber (the pre-standard form of ML-KEM).' }; + if ((k = /^M[lL][kK][eE][mM](768|1024)$/.exec(nume))) return { grup: 'MLKEM', param: k[1] }; + return null; + } + return null; +} + +export { CRATE as CRATE_RUST }; diff --git a/crypto-inventory/lib/lexer.mjs b/crypto-inventory/lib/lexer.mjs index afed0e0..9ba4698 100644 --- a/crypto-inventory/lib/lexer.mjs +++ b/crypto-inventory/lib/lexer.mjs @@ -15,6 +15,7 @@ export function curata(text, limbaj) { case 'java': return curataC(text, { textBlock: true }); case 'go': return curataC(text, { raw: true }); case 'python': return curataPython(text); + case 'rust': return curataRust(text); default: return { code: text, siruri: [] }; } } @@ -199,6 +200,73 @@ function curataPython(text) { return { code: out.join(''), siruri }; } +// Rust (0.3.0): comentariile bloc se IMBRICA; sirurile pot trece peste randuri si au forme brute (r"...", r#"..."#, br"...", +// cr#"..."#) si de octeti (b"..."); apostroful NU deschide un sir decat ca literal char ('a', '\n', '\u{1F600}', b'x'): altfel e +// o durata de viata ('a, 'static) sau o eticheta ('outer:), si un lexer C ar fi deschis acolo un "sir" pana la urmatorul apostrof. +function curataRust(text) { + const n = text.length; + const out = text.split(''); + const goleste = golitor(out); + const siruri = []; + let i = 0; + while (i < n) { + const c = text[i]; + const d = text[i + 1]; + if (d === '/' && c === '/') { // (scris invers fata de lexerul C: controlul negativ isi ancoreaza ciotul pe acela) + let j = i; + while (j < n && text[j] !== '\n') j++; + goleste(i, j); i = j; continue; + } + if (c === '/' && d === '*') { + let j = i + 2; + let ad = 1; + while (j < n && ad > 0) { + if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; } + if (text[j] === '*' && text[j + 1] === '/') { ad--; j += 2; continue; } + j++; + } + goleste(i, j); i = j; continue; + } + const dupaCuvant = i > 0 && /\w/.test(text[i - 1]); + if (!dupaCuvant && (c === 'r' || ((c === 'b' || c === 'c') && d === 'r'))) { + let j = c === 'r' ? i + 1 : i + 2; + let diezi = 0; + while (text[j] === '#') { diezi++; j++; } + if (text[j] === '"') { + const inchidere = '"' + '#'.repeat(diezi); + const k = text.indexOf(inchidere, j + 1); + const sf = k < 0 ? n : k + inchidere.length; + siruri.push([i, sf]); i = sf; continue; + } + } + if (c === '"' || (!dupaCuvant && (c === 'b' || c === 'c') && d === '"')) { + const start = i; + let j = c === '"' ? i + 1 : i + 2; + while (j < n && text[j] !== '"') { if (text[j] === '\\') j++; j++; } + j = Math.min(n, j + 1); + siruri.push([start, j]); i = j; continue; + } + if (c === "'") { + if (d === '\\') { + let j = i + 2; + while (j < n && text[j] !== "'" && text[j] !== '\n' && j - i < 16) j++; + if (text[j] === "'") { siruri.push([i, j + 1]); i = j + 1; continue; } + i++; continue; + } + if (d !== undefined && d !== '\n' && text[i + 2] === "'") { siruri.push([i, i + 3]); i += 3; continue; } + if (d !== undefined && d.charCodeAt(0) >= 0xd800 && d.charCodeAt(0) <= 0xdbff && text[i + 3] === "'") { siruri.push([i, i + 4]); i += 4; continue; } + i++; continue; + } + if (/[A-Za-z_]/.test(c)) { + let j = i + 1; + while (j < n && /\w/.test(text[j])) j++; + i = j; continue; + } + i++; + } + return { code: out.join(''), siruri }; +} + // cautare binara: pozitia e inauntrul unui sir? export function inSir(siruri, pos) { let lo = 0; diff --git a/crypto-inventory/lib/manifeste.mjs b/crypto-inventory/lib/manifeste.mjs index 22ffedb..d0aac3b 100644 --- a/crypto-inventory/lib/manifeste.mjs +++ b/crypto-inventory/lib/manifeste.mjs @@ -43,15 +43,33 @@ const GO = [ [/^filippo\.io\/edwards25519$/, 'Edwards25519 group arithmetic'], [/^github\.com\/open-quantum-safe\/liboqs-go$/, 'post-quantum KEMs and signatures (liboqs)'], ]; +// Cargo (0.3.0): crate-urile criptografice cunoscute de detectorul Rust, cu numele lor din registru (liniuta, nu liniuta jos), plus +// cele care se vad numai in manifest (furnizori, legaturi). Versiunea citita e CERINTA declarata ("0.10", "^1.2"), nu versiunea +// rezolvata din Cargo.lock; purl-ul o poarta numai cand e un numar simplu. +import { CRATE_RUST } from './detect-rust.mjs'; +const CARGO = [ + ...Object.entries(CRATE_RUST).map(([c, v]) => [new RegExp('^' + c.replace(/_/g, '-') + '$'), v.ofera]), + [/^md-5$/, 'MD5'], + [/^cfb-mode$/, 'the CFB mode'], + [/^aws-lc-rs$/, 'AWS-LC: AES-GCM, ChaCha20-Poly1305, SHA-2, HMAC, HKDF, Ed25519, ECDSA, RSA, X25519, ML-KEM, ML-DSA'], + [/^rustls-(webpki|pki-types|native-certs|platform-verifier)$/, 'certificate handling for rustls'], + [/^webpki(-roots)?$/, 'WebPKI certificate validation (roots or verifier)'], + [/^openssl-sys$/, 'OpenSSL bindings'], + [/^pqcrypto$/, 'PQClean post-quantum KEMs and signatures (meta-crate)'], + [/^(x509-parser|x509-cert|rcgen)$/, 'X.509 certificates'], + [/^(ed25519|ecdsa|elliptic-curve|signature|aead|cipher|digest|kem)$/, 'RustCrypto trait crate (algorithm chosen by the implementation crate)'], + [/^(sodiumoxide|libsodium-sys|dryoc)$/, 'libsodium (NaCl): X25519, Ed25519, XSalsa20/XChaCha20-Poly1305, Argon2'], +]; -export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|go\.mod)$/; +export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|go\.mod|Cargo\.toml)$/; function lib(ecosistem, nume, versiune, ofera, pos, inceputuri, fisier) { const { line } = randul(inceputuri, pos); const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` : ecosistem === 'pypi' ? `pkg:pypi/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` : ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` - : `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`; + : ecosistem === 'cargo' ? `pkg:cargo/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}` + : `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`; return { file: fisier, line, ecosystem: ecosistem, name: nume, version: versiune || undefined, provides: ofera, purl }; } @@ -129,6 +147,55 @@ export function citesteManifest(text, numeFisier, rel) { } pos += linie.length + 1; } + } else if (numeFisier === 'Cargo.toml') { + // sectiunile [dependencies], [dev-dependencies], [build-dependencies], [workspace.dependencies], [target.'cfg(...)'.dependencies] + // si forma tabel [dependencies.nume] cu `version =` / `package =` pe randurile urmatoare + let pos = 0; + let inDependente = false; + let tabel = null; // { nume, pos, versiune, pachet } pentru [dependencies.nume] + const inchide = () => { + if (!tabel) return; + const nume = tabel.pachet || tabel.nume; + const intrare = CARGO.find(([re]) => re.test(nume)); + if (intrare) r.push(lib('cargo', nume, tabel.versiune, intrare[1], tabel.pos, inceputuri, rel)); + tabel = null; + }; + const valoare = (s) => { const v = /^\s*["']([^"']*)["']/.exec(s); return v ? v[1].trim() : undefined; }; + for (const linie of text.split('\n')) { + const t = linie.replace(/\s+#.*$/, '').replace(/^#.*$/, '').trim(); + let h; + if ((h = /^\[\s*(.+?)\s*\]$/.exec(t))) { + inchide(); + const s = h[1].replace(/\s+/g, ''); + const dep = /^(?:workspace\.|target\.(?:'[^']*'|"[^"]*"|[^.]+)\.)?(dependencies|dev-dependencies|build-dependencies)(?:\.(?:"([^"]+)"|([\w.-]+)))?$/.exec(s); + inDependente = !!dep; + const numeTabel = dep && (dep[2] || dep[3]); + if (numeTabel) tabel = { nume: numeTabel, pos: pos + linie.indexOf(numeTabel), versiune: undefined, pachet: null }; + } else if (tabel) { + let m; + if ((m = /^version\s*=\s*(.+)$/.exec(t))) tabel.versiune = valoare(m[1]); + else if ((m = /^package\s*=\s*(.+)$/.exec(t))) tabel.pachet = valoare(m[1]); + } else if (inDependente && t) { + const m = /^(?:"([^"]+)"|([\w.-]+))\s*=\s*(.+)$/.exec(t); + if (m) { + let nume = m[1] || m[2]; + let versiune; + const val = m[3].trim(); + if (val.startsWith('{')) { + const v = /\bversion\s*=\s*["']([^"']*)["']/.exec(val); + const pk = /\bpackage\s*=\s*["']([^"']*)["']/.exec(val); + if (pk) nume = pk[1]; + versiune = v ? v[1].trim() : undefined; + } else { + versiune = valoare(val); + } + const intrare = CARGO.find(([re]) => re.test(nume)); + if (intrare) r.push(lib('cargo', nume, versiune, intrare[1], pos + linie.indexOf(m[1] || m[2]), inceputuri, rel)); + } + } + pos += linie.length + 1; + } + inchide(); } return { biblioteci: r }; } diff --git a/crypto-inventory/lib/scan.mjs b/crypto-inventory/lib/scan.mjs index fbf7643..da7197e 100644 --- a/crypto-inventory/lib/scan.mjs +++ b/crypto-inventory/lib/scan.mjs @@ -7,11 +7,12 @@ import { detecteazaJs } from './detect-js.mjs'; import { detecteazaPy } from './detect-py.mjs'; import { detecteazaJava } from './detect-java.mjs'; import { detecteazaGo } from './detect-go.mjs'; +import { detecteazaRust } from './detect-rust.mjs'; import { detecteazaPem } from './pem.mjs'; import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs'; -export const VERSIUNE = '0.2.0'; -export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache']; +export const VERSIUNE = '0.3.0'; +export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache', '.claude']; const LIMBAJ = { '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript', @@ -19,8 +20,9 @@ const LIMBAJ = { '.py': 'python', '.pyw': 'python', '.java': 'java', '.go': 'go', + '.rs': 'rust', }; -const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo }; +const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo, rust: detecteazaRust }; const EXEMPLE = 5; export function limbajul(cale) { diff --git a/crypto-inventory/test/control-negativ.mjs b/crypto-inventory/test/control-negativ.mjs index c6e863c..b1100dc 100644 --- a/crypto-inventory/test/control-negativ.mjs +++ b/crypto-inventory/test/control-negativ.mjs @@ -16,6 +16,30 @@ const SUITA = join(AICI, 'proba.mjs'); const B = 'negativ: fisierul binar e sarit si numarat'; const MUTATII = [ + { + nume: 'comentariile bloc Rust nu se mai imbrica (lexerul)', + fisier: 'lib/lexer.mjs', + ancora: "if (text[j] === '/' && text[j + 1] === '*') { ad++; j += 2; continue; }", + cu: "if (false) { ad++; j += 2; continue; }", + tinta: 'negativ: comentariile, sirurile brute, literalele char si lifetime-urile Rust nu produc gasiri', + martor: B, + }, + { + nume: 'SigningKey clasificat dupa nume, nu dupa crate-ul din use (Rust)', + fisier: 'lib/detect-rust.mjs', + ancora: "if (u) return CRATE[u.crate] ? { crate: u.crate, cale: u.cale } : null;", + cu: "if (u) return { crate: 'ed25519_dalek', cale: ['ed25519_dalek', tip] };", + tinta: 'rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey)', + martor: B, + }, + { + nume: 'Cargo.toml scos din manifeste', + fisier: 'lib/manifeste.mjs', + ancora: '|go\\.mod|Cargo\\.toml)$/;', + cu: '|go\\.mod)$/;', + tinta: 'pozitiv: manifestele dau biblioteci declarate, nu gasiri', + martor: B, + }, { nume: 'secp256k1 clasificat quantum-safe', fisier: 'lib/catalog.mjs', diff --git a/crypto-inventory/test/echivalenta-0.1.0.mjs b/crypto-inventory/test/echivalenta-0.1.0.mjs index 74fa0b2..331fc36 100644 --- a/crypto-inventory/test/echivalenta-0.1.0.mjs +++ b/crypto-inventory/test/echivalenta-0.1.0.mjs @@ -27,6 +27,7 @@ const fis = ls.status === 0 ? ls.stdout.toString().split('\n').filter(Boolean) : if (!fis.length) { console.log(`SARIT: revizia ${REV} nu e in istoricul acestui depozit; NEMASURAT aici`); process.exit(2); } const vechi = mkdtempSync(join(tmpdir(), 'ci-v010-')); let dif = 0, comparate = 0; +let versiuneNoua = '?'; try { for (const f of fis) { const b = git('show', `${REV}:${f}`); @@ -37,12 +38,19 @@ try { } const imp = (r, m) => import(pathToFileURL(join(r, 'lib', m)).href); const [SV, SN, LV, LN, PN] = await Promise.all([imp(vechi, 'scan.mjs'), imp(RAD, 'scan.mjs'), imp(vechi, 'lexer.mjs'), imp(RAD, 'lexer.mjs'), imp(RAD, 'pem.mjs')]); + versiuneNoua = SN.VERSIUNE; if (SV.VERSIUNE !== '0.1.0') { console.log(`NEMASURAT: revizia ${REV} are versiunea ${SV.VERSIUNE}, nu 0.1.0`); process.exit(2); } // 1. arbori reali const norm = (g) => JSON.stringify({ ...g, reason: String(g.reason).replace(/, which (cannot be resolved statically|this tool does not resolve statically).*$/, ', which [motiv]') }); for (const d of DOSARE) { const a = SV.scaneaza(d), b = SN.scaneaza(d); + // 0.3.0 (2026-10-18): Rust si Cargo.toml nu existau in 0.1.0, deci gasirile lor nu intra in echivalenta; se numara si se spun + const rustN = b.findings.filter((g) => g.language === 'rust').length; + const cargoN = b.libraries.filter((x) => x.ecosystem === 'cargo').length; + b.findings = b.findings.filter((g) => g.language !== 'rust'); + b.libraries = b.libraries.filter((x) => x.ecosystem !== 'cargo'); + 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)); @@ -50,7 +58,7 @@ try { 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}`); - for (const x of [...doarV.slice(0, 3).map((x) => 'numai 0.1.0: ' + x), ...doarN.slice(0, 3).map((x) => 'numai 0.2.0: ' + x)]) console.log(' ' + x.slice(0, 260)); + for (const x of [...doarV.slice(0, 3).map((x) => 'numai 0.1.0: ' + x), ...doarN.slice(0, 3).map((x) => `numai ${SN.VERSIUNE}: ` + x)]) console.log(' ' + x.slice(0, 260)); } // 2. texte generate: lexerul pe patru limbaje si cautarea PEM (fata de regexul din 0.1.0) @@ -83,5 +91,5 @@ try { } console.log(`texte generate: ${ITER} runde (lexer x4 + PEM), diferente ${JSON.stringify(cat)}`); } finally { rmSync(vechi, { recursive: true, force: true }); } -console.log(`\n${dif ? 'DIFERIT' : 'IDENTIC'}: ${dif} diferente in ${comparate} comparatii (0.1.0 din ${REV} fata de 0.2.0)`); +console.log(`\n${dif ? 'DIFERIT' : 'IDENTIC'}: ${dif} diferente in ${comparate} comparatii (0.1.0 din ${REV} fata de ${versiuneNoua})`); process.exitCode = dif ? 1 : comparate ? 0 : 2; diff --git a/crypto-inventory/test/fixturi/manifeste/Cargo.toml b/crypto-inventory/test/fixturi/manifeste/Cargo.toml new file mode 100644 index 0000000..e0e48bd --- /dev/null +++ b/crypto-inventory/test/fixturi/manifeste/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "fixtura" +version = "0.1.0" +edition = "2021" + +[dependencies] +sha2 = "0.10" +ed25519-dalek = { version = "2", features = ["rand_core"] } +rustls = { version = "0.23", default-features = false, features = ["ring"] } +tokio = { version = "1", features = ["full"] } +aes-gcm = { workspace = true } +pq = { package = "pqcrypto-mlkem", version = "0.1" } +serde = "1" + +[dependencies.ml-dsa] +version = "0.1.1" +default-features = false + +[dev-dependencies] +hex-literal = "0.4" +argon2 = "0.5" diff --git a/crypto-inventory/test/fixturi/negative/comentarii.rs b/crypto-inventory/test/fixturi/negative/comentarii.rs new file mode 100644 index 0000000..c3f0a00 --- /dev/null +++ b/crypto-inventory/test/fixturi/negative/comentarii.rs @@ -0,0 +1,23 @@ +// Fixtura negativa Rust: nicio folosire reala. Comentarii (si imbricate), siruri obisnuite, brute si de octeti, literale char +// (inclusiv '"' si '\''), durate de viata si etichete: niciuna nu are voie sa produca o gasire sau sa deschida un sir. +use std::fmt; + +/* use sha2::Sha256; + /* let h = Sha256::new(); */ + let d = Sha1::digest(b"x"); +*/ + +/// Aes256Gcm::new_from_slice(key) intr-un comentariu de documentatie nu e o folosire. +pub fn nimic<'a>(x: &'a str, y: &'a str) -> &'a str { + let ghilimea = '"'; + let apostrof = '\''; + let brut = r#"Sha256::new() si "Hmac::::new_from_slice""#; + let octeti = b"RsaPrivateKey::new(&mut rng, 1024)"; + let text = "use ed25519_dalek::SigningKey; SigningKey::generate(rng)"; + 'exterior: loop { + if ghilimea == apostrof { break 'exterior; } + break; + } + let _ = (brut, octeti, text, fmt::Error); + if x.len() > y.len() { x } else { y } +} diff --git a/crypto-inventory/test/fixturi/rust/ambiguu.rs b/crypto-inventory/test/fixturi/rust/ambiguu.rs new file mode 100644 index 0000000..45e7da1 --- /dev/null +++ b/crypto-inventory/test/fixturi/rust/ambiguu.rs @@ -0,0 +1,6 @@ +// Fixtura: Rust, fara niciun `use` care sa spuna din ce crate vine SigningKey. Numele e purtat de ed25519-dalek, ml-dsa, slh-dsa, +// p256, k256 si rsa, cu intelesuri diferite; unealta nu ghiceste, raporteaza unknown cu motivul scris. +pub fn cheie(b: &[u8; 32]) -> Vec { + let k = SigningKey::from_bytes(b); // EXPECT: SigningKey (crate not resolved) | unknown + k.to_bytes().to_vec() +} diff --git a/crypto-inventory/test/fixturi/rust/cripto.rs b/crypto-inventory/test/fixturi/rust/cripto.rs new file mode 100644 index 0000000..4164a40 --- /dev/null +++ b/crypto-inventory/test/fixturi/rust/cripto.rs @@ -0,0 +1,53 @@ +// Fixtura: Rust. Numele din scop vin din `use`, si acelasi nume inseamna alt algoritm dupa crate (SigningKey e Ed25519 in +// ed25519-dalek, ML-DSA in ml-dsa, ECDSA in p256). Un import fara niciun apel recunoscut ramane dovada slaba (unknown). +use sha2::{Digest, Sha256, Sha512}; +use sha1::Sha1; +use hmac::{Hmac, Mac}; +use hkdf::Hkdf; +use argon2::{Algorithm, Argon2, Params, Version}; +use chacha20poly1305::{aead::KeyInit, XChaCha20Poly1305}; +use aes_gcm::Aes256Gcm; +use x25519_dalek::{PublicKey, StaticSecret}; +use ed25519_dalek::SigningKey; +use ml_dsa::{KeyGen, MlDsa65}; +use slh_dsa::{Sha2_128s, SigningKey as SlhSigningKey}; +use ml_kem::{KemCore, MlKem768}; +use rsa::RsaPrivateKey; +use p256::ecdsa::SigningKey as P256SigningKey; +use libsignal_protocol::kem; +use ring::aead; +use jsonwebtoken::Algorithm as JwtAlg; +use openssl::hash::MessageDigest; +use rustls::crypto::ring as rustls_ring; +use snow::Builder; +use blake2::Blake2b512; // EXPECT: blake2 | unknown +use zeroize::Zeroizing; + +type HmacSha256 = Hmac; // EXPECT: HMAC-SHA-256 | quantum-safe + +pub fn exemple(data: &[u8], key: &[u8; 32], rng: &mut impl rand_core::CryptoRngCore) { + let h = Sha256::new(); // EXPECT: SHA-256 | quantum-safe + let d = Sha1::digest(data); // EXPECT: SHA-1 | weak-now + let m = Hmac::::new_from_slice(key).unwrap(); // EXPECT: HMAC-SHA-512 | quantum-safe + let hk = Hkdf::::new(None, key); // EXPECT: HKDF | quantum-safe + let argon = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default()); // EXPECT: Argon2id | quantum-safe + let aead1 = XChaCha20Poly1305::new_from_slice(key).unwrap(); // EXPECT: XChaCha20-Poly1305 | quantum-safe + let aead2 = Aes256Gcm::new_from_slice(key).unwrap(); // EXPECT: AES-256-GCM | quantum-safe + let bloc = ecb::Encryptor::::new(key[..16].into()); // EXPECT: AES-128-ECB | weak-now + let secret = StaticSecret::random_from_rng(rng); // EXPECT: X25519 | quantum-vulnerable + let pk = PublicKey::from(&secret); // EXPECT: X25519 | quantum-vulnerable + let ed = SigningKey::generate(rng); // EXPECT: Ed25519 | quantum-vulnerable + let kp = MlDsa65::key_gen(rng); // EXPECT: ML-DSA-65 | quantum-safe + let slh = SlhSigningKey::::new(rng); // EXPECT: SLH-DSA-SHA2-128s | quantum-safe + let (dk, ek) = MlKem768::generate(rng); // EXPECT: ML-KEM-768 | quantum-safe + let r = RsaPrivateKey::new(rng, 2048).unwrap(); // EXPECT: RSA-2048 | quantum-vulnerable + let p = P256SigningKey::random(rng); // EXPECT: ECDSA-secp256r1 | quantum-vulnerable + let kt = kem::KeyType::Kyber1024; // EXPECT: Kyber-1024 | quantum-safe + let sealing = aead::UnboundKey::new(&aead::AES_256_GCM, key).unwrap(); // EXPECT: AES-256-GCM | quantum-safe + let alg = JwtAlg::RS256; // EXPECT: JWS RS256 | quantum-vulnerable + let md = MessageDigest::md5(); // EXPECT: MD5 | weak-now + let provider = rustls_ring::default_provider(); // EXPECT: X25519 | quantum-vulnerable + let noise = Builder::new("Noise_XX_25519_ChaChaPoly_SHA256".parse().unwrap()); // EXPECT: X25519 | quantum-vulnerable ; ChaCha20-Poly1305 | quantum-safe ; SHA-256 | quantum-safe + let _ = (h, d, m, hk, argon, aead1, aead2, bloc, pk, ed, kp, slh, dk, ek, r, p, kt, sealing, alg, md, provider, noise, Zeroizing::new(0u8)); + let _t: Option = None; +} diff --git a/crypto-inventory/test/proba.mjs b/crypto-inventory/test/proba.mjs index 27aed24..5eae559 100644 --- a/crypto-inventory/test/proba.mjs +++ b/crypto-inventory/test/proba.mjs @@ -14,11 +14,12 @@ const AICI = dirname(fileURLToPath(import.meta.url)); const FIX = join(AICI, 'fixturi'); const CALE_MODUL = process.env.CRYPTO_INVENTORY_MODULE ? resolve(process.env.CRYPTO_INVENTORY_MODULE) : join(AICI, '..', 'inventar.mjs'); const inv = await import(pathToFileURL(CALE_MODUL).href); +const lx = await import(pathToFileURL(join(dirname(CALE_MODUL), 'lib', 'lexer.mjs')).href); 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 = 96; -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', 'negative/variabile.js']; +const MARCAJE_ASTEPTATE = 123; +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', 'negative/variabile.js']; const probe = []; const proba = (nume, fn) => probe.push({ nume, fn }); @@ -89,11 +90,12 @@ proba('pozitiv: manifestele dau biblioteci declarate, nu gasiri', () => { const libs = rez.libraries.map((b) => `${b.file}:${b.line} ${b.name}`).sort(); const ast = ['go.mod:6 github.com/cloudflare/circl', 'go.mod:7 golang.org/x/crypto', 'package.json:10 @noble/post-quantum', 'package.json:5 ethers', 'package.json:7 jsonwebtoken', 'pom.xml:5 org.bouncycastle:bcprov-jdk18on', - 'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt'].sort(); + 'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt', 'Cargo.toml:7 sha2', 'Cargo.toml:8 ed25519-dalek', 'Cargo.toml:9 rustls', + 'Cargo.toml:11 aes-gcm', 'Cargo.toml:12 pqcrypto-mlkem', 'Cargo.toml:15 ml-dsa', 'Cargo.toml:21 argon2'].sort(); asigura(JSON.stringify(libs) === JSON.stringify(ast), `biblioteci: ${libs.join(' | ')}`); const bom = inv.buildCbom(rez, { deterministic: true }); const c = bom.components.filter((x) => x.type === 'library'); - asigura(c.length === 8 && c.every((x) => x.properties.some((p) => p.name.endsWith('declared-only') && p.value === 'true')), 'componentele library nu poarta declared-only'); + asigura(c.length === 15 && c.every((x) => x.properties.some((p) => p.name.endsWith('declared-only') && p.value === 'true')), 'componentele library nu poarta declared-only'); }); proba('pozitiv: material PEM (certificat si cheie publica)', () => { @@ -192,9 +194,58 @@ proba('negativ: comentariile si sirurile Java si Go nu produc gasiri', () => { asigura(g.length === 0, `au produs: ${g.map((x) => `${x.file}:${x.line} ${x.name}`).join(', ')}`); }); +proba('negativ: comentariile, sirurile brute, literalele char si lifetime-urile Rust nu produc gasiri', () => { + const rez = scan(FIX); + asigura(gasiriDin(rez, 'rust/cripto.rs').some((x) => x.name === 'MD5'), 'controlul pozitiv: MD5 din fixtura Rust lipseste'); + const g = gasiriDin(rez, 'negative/comentarii.rs'); + asigura(g.length === 0, `au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`); +}); + +proba('rust: lexerul: lifetime-urile si literalele char nu deschid siruri, comentariile bloc se imbrica, sirurile brute se inchid la diezi', () => { + const t1 = "fn f<'a>(x: &'a str) { let c = '\"'; let s = \"Sha256::new()\"; Sha1::digest(x) }"; + const c1 = lx.curata(t1, 'rust'); + asigura(lx.inSir(c1.siruri, t1.indexOf('Sha256')) && !lx.inSir(c1.siruri, t1.indexOf('Sha1')), `t1: siruri ${JSON.stringify(c1.siruri)}`); + const t2 = '/* a /* b */ Sha256::new() */ Md5::new()'; + const c2 = lx.curata(t2, 'rust'); + asigura(!c2.code.includes('Sha256') && c2.code.includes('Md5') && c2.code.length === t2.length, `t2: ${JSON.stringify(c2.code)}`); + const t3 = 'let s = r#"a"b"#; let o = b"x\\"y"; Sha1::digest(d)'; + const c3 = lx.curata(t3, 'rust'); + asigura(c3.siruri.length === 2 && !lx.inSir(c3.siruri, t3.indexOf('Sha1')), `t3: ${JSON.stringify(c3.siruri)}`); + const t4 = "let a = '\\''; let e = '\\u{1F600}'; 'outer: loop { break 'outer; } Md5::new()"; + const c4 = lx.curata(t4, 'rust'); + asigura(c4.siruri.length === 2 && !lx.inSir(c4.siruri, t4.indexOf('Md5')), `t4: ${JSON.stringify(c4.siruri)}`); +}); + +proba('rust: acelasi nume de tip e clasificat dupa crate-ul din use (SigningKey)', () => { + const g = gasiriDin(scan(FIX), 'rust/cripto.rs'); + const nume = (api) => g.filter((x) => x.api === api).map((x) => x.name); + asigura(JSON.stringify(nume('SigningKey::generate')) === '["Ed25519"]', `ed25519_dalek: ${nume('SigningKey::generate')}`); + asigura(JSON.stringify(nume('P256SigningKey::random')) === '["ECDSA-secp256r1"]', `p256 prin alias: ${nume('P256SigningKey::random')}`); + asigura(JSON.stringify(nume('SlhSigningKey::new')) === '["SLH-DSA-SHA2-128s"]', `slh_dsa prin alias + generic: ${nume('SlhSigningKey::new')}`); + const a = gasiriDin(scan(FIX), 'rust/ambiguu.rs'); + 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: 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'); + asigura(imp.length === 1 && imp[0].name === 'blake2' && imp[0].classification === 'unknown', `importuri: ${JSON.stringify(imp.map((x) => [x.name, x.classification]))}`); + asigura(!g.some((x) => x.api === 'use sha2' || x.api === 'use ed25519_dalek'), 'sha2 / ed25519_dalek au apeluri, importul lor nu trebuie raportat'); +}); + +proba('rust: Cargo.toml: forma tabel, workspace = true fara versiune, redenumire prin package, purl pkg:cargo', () => { + const libs = scan(join(FIX, 'manifeste')).libraries.filter((b) => b.ecosystem === 'cargo'); + const l = (n) => libs.find((b) => b.name === n); + asigura(l('ml-dsa') && l('ml-dsa').version === '0.1.1' && l('ml-dsa').line === 15, `tabel: ${JSON.stringify(l('ml-dsa'))}`); + asigura(l('aes-gcm') && l('aes-gcm').version === undefined && l('aes-gcm').purl === 'pkg:cargo/aes-gcm', `workspace: ${JSON.stringify(l('aes-gcm'))}`); + asigura(l('pqcrypto-mlkem') && l('pqcrypto-mlkem').version === '0.1' && !l('pq'), `package =: ${JSON.stringify(l('pqcrypto-mlkem'))}`); + asigura(l('sha2') && l('sha2').purl === 'pkg:cargo/sha2@0.10' && l('argon2') && l('argon2').line === 21, `purl / dev-dependencies: ${JSON.stringify([l('sha2'), l('argon2')])}`); + asigura(!l('tokio') && !l('serde') && !l('hex-literal'), 'crate-urile fara criptografie nu sunt biblioteci declarate'); +}); + proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => { const rez = scan(join(FIX, 'negative')); - asigura(rez.stats.filesSeen === 7 && rez.stats.codeFilesTotal === 7, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`); + asigura(rez.stats.filesSeen === 8 && rez.stats.codeFilesTotal === 8, `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}`); });