ecdsa-scan · outil CLI gratuit · aucune dépendance · Node 20+
Trouvez chaque endroit où votre code signe – et ce qui y est faux.
Les scanners de secrets trouvent les clés commitées. ecdsa-scan s’intéresse à la couche du dessus : le code de signature subtilement faux, comme un JWT vérifié sans épingler l’algorithme, une clé P-256 générée dans du code Ethereum, une signature r‖s construite à la main sans remplissage ou une vérification dont le résultat booléen est jeté. Lecture seule, aucune étape de build, aucune configuration. JavaScript, TypeScript, Python et Go.
$ npx ecdsa-scan .Ni installation ni compte. Code de sortie 1 uniquement pour les constats de niveau confirmed : prêt à servir de contrôle bloquant en CI. Installation globale : npm i -g ecdsa-scan
Des constats classés et expliqués
Chaque constat comporte un niveau de confiance, une explication du mécanisme en anglais clair, un extrait de code corrigé et un lien vers la RFC concernée. Jamais une simple correspondance de motif sans explication.
Du SARIF pour l’analyse de code
Une seule option produit du SARIF 2.1.0 pour GitHub Code Scanning : confirmed → error, suspected → warning, advisory → note, avec le correctif intégré à chaque annotation.
Une amorce de CBOM offerte
Le rapport JSON recense, fichier par fichier, les bibliothèques cryptographiques, les algorithmes, les courbes et les opérations de signature qui y apparaissent : la matière première de la carte de migration post-quantique.
À quoi ressemble une analyse
Une sortie réelle, capturée telle quelle pendant que ecdsa-scan 0.1.0 analyse deux fichiers de sa propre suite de fixtures de test. Chaque constat nomme le mécanisme, montre le correctif et cite la norme. Le scanner lui-même rédige ses rapports en anglais.
$ npx ecdsa-scan . src/auth/session.js 6:18 confirmed jwt-verify-missing-algorithms JWT verified without an explicit algorithm allow-list │ const claims = jwt.verify(token, publicKey); `jwt.verify(token, publicKey)` does not pass `algorithms`, so the token header decides how the signature is checked. Why it matters: A JWT names its own algorithm in the header, so a verifier that does not pin the accepted algorithms lets the token choose how it is checked. The classic result is algorithm confusion: an attacker re-signs the token with HS256 using your RSA/EC public key as the HMAC secret, or supplies alg:none, and verification succeeds. Always pass the exact algorithms your issuer uses. Fix: jsonwebtoken: jwt.verify(token, publicKey, { algorithms: ["ES256"] }) jose: await jwtVerify(token, key, { algorithms: ["ES256"] }) PyJWT: jwt.decode(token, key, algorithms=["ES256"]) golang-jwt: jwt.Parse(s, keyFunc, jwt.WithValidMethods([]string{"ES256"})) Reference: https://datatracker.ietf.org/doc/html/rfc8725#section-3.1 11:29 confirmed jwt-verify-missing-algorithms JWT verified without an explicit algorithm allow-list │ const { payload } = await jwtVerify(token, key); `jwtVerify(token, key)` does not pass `algorithms`, so the token header decides how the signature is checked. 17:10 confirmed jwt-verify-missing-algorithms JWT verified without an explicit algorithm allow-list │ return jwt.verify(token, key, { algorithms: [] }); `jwt.verify` accepts an empty algorithm list or "none" — every token, including unsigned ones, will pass. src/webhooks/verify.js 6:3 suspected unchecked-verification-result Verification result is never checked │ crypto.verify(null, data, publicKey, signature); `crypto.verify(...)` returns a boolean that is discarded here, so a failed signature check changes nothing. Wrap it in a condition and reject on `false`. Why it matters: `crypto.verify`, `secp256k1.verify` and `ecdsa.Verify` report failure by returning `false`, not by throwing. Calling one as a bare statement means an invalid signature is indistinguishable from a valid one and execution simply continues — the check exists in the code but does nothing. The result must control a branch: an `if`, an early return, or a thrown error. Fix: // bad crypto.verify(null, data, publicKey, signature); // good if (!crypto.verify(null, data, publicKey, signature)) { throw new Error('invalid signature'); } Reference: https://nodejs.org/api/crypto.html#cryptoverifyalgorithm-data-key-signature-callback Inventory library: jose (1), jsonwebtoken (1), node:crypto (1) operation: verify (2) Summary 2 files scanned, 4 findings (3 confirmed, 1 suspected) Exit code 1: 3 confirmed findings. Static pattern analysis: it can miss defects and can flag correct code. Treat advisory findings as questions, not verdicts.
Trois niveaux de confiance
Les constats sont classés, et ce classement constitue notre engagement envers vous. Un scanner que l’on réduit au silence ne vaut rien : seuls les défauts sans ambiguïté peuvent donc faire échouer un build.
Le motif est un défaut, quel que soit le code environnant.
Corrigez-le, et faites-en un contrôle bloquant en CI : seuls les constats de niveau confirmed donnent un code de sortie non nul.
Très probablement faux ; le code environnant tranche.
Lisez le constat, puis corrigez le code ou écartez le constat.
Mérite un examen : du code légitime correspond aussi à ce motif.
Traitez-le comme une question, pas comme un verdict.
Les 15 règles
Quatorze règles de détection de défauts et un collecteur d’inventaire. ecdsa-scan rules affiche la même liste. Certaines règles font changer un constat de niveau selon le contexte : une clé privée sous test/fixtures/ est classée advisory, et non confirmed.
| Règle | Confiance | Gravité | Ce qu’elle détecte |
|---|---|---|---|
| jwt-verify-missing-algorithms | confirmed | élevée | jwt.verify / jwtVerify sans algorithms:, decode de PyJWT sans algorithms=, jwt.Parse sans WithValidMethods, ainsi que les listes vides ou réduites à « none ». |
| jwt-alg-from-token | confirmed | élevée | La liste des algorithmes de vérification construite à partir de l’en-tête du jeton lui-même (algorithms: [header.alg], get_unverified_header). |
| jwt-decode-without-verification | suspected | élevée | jwt.decode, decodeJwt, jwtDecode, PyJWT verify_signature: False ; niveau relevé quand des identifiants liés aux rôles, aux utilisateurs ou aux permissions consomment le résultat. |
| insecure-nonce-source | confirmed | élevée | Math.random(), random de Python, math/rand à proximité de données de clé ou de nonce ; k fourni par l’appelant, extraEntropy, deterministic: false. |
| hardcoded-private-key | confirmed | élevée | Blocs PEM de clé privée dans le code source ; littéraux hexadécimaux de 64 caractères affectés à des noms du type privateKey/secret/signingKey. |
| key-file-outside-tests | suspected | élevée | Fichiers .pem/.key/.p12/id_ecdsa contenant des données de clé privée en dehors des répertoires test/, fixtures/ et examples/. |
| unchecked-verification-result | suspected | élevée | Vérification renvoyant un booléen (crypto.verify, ecdsa.VerifyASN1, …) appelée comme simple instruction, son résultat étant ignoré. |
| weak-signature-hash | confirmed | élevée | SHA-1 ou MD5 sur un chemin de signature : createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA ; condensats SHA-1 ailleurs dans le code de signature. |
| tls-verification-disabled | confirmed | élevée | rejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False sur les clients HTTP, ssl.CERT_NONE, InsecureSkipVerify: true. |
| signature-encoding | suspected | moyenne | r et s concaténés à la main sans remplissage par des zéros jusqu’à la taille du corps ; crypto.sign/verify de Node sans dsaEncoding dans du code JWS. |
| non-constant-time-comparison | suspected | moyenne | Signatures, MAC ou condensats comparés avec ==, ===, .equals() ou bytes.Equal au lieu d’une fonction de comparaison en temps constant. |
| curve-mixing | advisory | moyenne | Une clé P-256 créée dans du code Ethereum/Bitcoin ; secp256k1 et P-256 manipulées dans le même module. |
| unvalidated-public-key-point | advisory | moyenne | Clés publiques construites à partir de coordonnées x/y brutes sans vérifier que le point est sur la courbe ; arithmétique de courbe codée à la main. |
| secp256k1-low-s | advisory | faible | Signature ou vérification secp256k1 sans normalisation low-S visible : une question de malléabilité pour le code blockchain. |
| crypto-inventory | n/a | n/a | Pas une règle de défaut : recense, fichier par fichier, les bibliothèques cryptographiques, les algorithmes, les courbes et les opérations de signature, soit l’amorce du CBOM. |
Le niveau de confiance affiché est celui par défaut ; le code environnant peut relever ou abaisser le niveau de chaque constat.
Intégration en CI
Le scanner ne renvoie le code 1 que s’il existe un défaut de niveau confirmed : il s’intègre donc à n’importe quel pipeline sans problème de bruit. Le rapport SARIF transforme les constats en annotations de pull request, chacune avec son propre correctif et sa référence.
GitHub Actions
Analyse à chaque push et à chaque PR, publication des constats dans GitHub Code Scanning.
# .github/workflows/ecdsa-scan.yml
name: ecdsa-scan
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
security-events: write # required by upload-sarif
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: ECDSA signature scan
run: npx ecdsa-scan . --sarif ecdsa.sarif
continue-on-error: true # keep the upload step running on findings
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ecdsa.sarifBitbucket Pipelines
Échec de l’étape de PR en cas de constat de niveau confirmed, SARIF conservé comme artefact.
# bitbucket-pipelines.yml
pipelines:
pull-requests:
"**":
- step:
name: ECDSA signature scan
image: node:20
script:
# exits 1 on confirmed findings and fails the step
- npx ecdsa-scan . --sarif ecdsa.sarif
artifacts:
- ecdsa.sarifLimites : à lire
Il s’agit de recherche de motifs dans le texte, pas d’analyse de programme. Voici, en toute honnêteté, ce que cela apporte et ce que cela coûte :
Les faux positifs existent.
Un module qui prend légitimement en charge plusieurs courbes déclenche curve-mixing ; un outil de débogage appelle légitimement jwt.decode. C’est pourquoi chaque constat porte un niveau de confiance, pourquoi seuls les constats de niveau confirmed font échouer le build et pourquoi les constats de niveau advisory sont formulés comme des questions.
Les faux négatifs existent, et ils sont pires.
Tout ce qui est indirect est invisible : une fonction wrapper définie dans un autre fichier, un algorithme lu dans la configuration, un type de clé déterminé à l’exécution, un défaut qui passe par une bibliothèque dont le scanner n’a jamais entendu parler.
Un aléa faible n’est que partiellement détectable.
Un nonce biaisé produit par un PRNG maison situé trois modules plus loin ressemble exactement à du code correct. Les problèmes statistiques de nonce se détectent en examinant les signatures, pas le code source.
Ni analyse inter-fichiers, ni flux de données, ni suivi de contamination (taint tracking).
Chaque fichier est jugé isolément. C’est le prix à payer pour s’exécuter instantanément sur n’importe quel dépôt, dans n’importe quel état, sans installer ses dépendances ni rien compiler.
Le masquage des commentaires et des littéraux est heuristique.
Une mise en forme inhabituelle (une regex que l’analyseur lexical lit mal, un littéral de gabarit contenant une vraie logique) peut masquer un constat.
Réussir l’analyse ne vaut pas un audit.
Le scanner vérifie l’absence de certaines erreurs bien connues, pas la justesse de votre cryptographie. Considérez sa sortie comme une liste de lecture priorisée pour un relecteur humain.
Feuille de route
Le scanner est la première étape : la plateforme d’inventaire de signature arrive.
Une analyse vous indique où votre code signe aujourd’hui. La plateforme que nous construisons tient cette carte à jour : un inventaire unique, du dépôt à la clé puis à l’environnement, qui montre quelles clés existent, ce qu’elles signent, quels algorithmes elles utilisent et ce qui doit migrer avant les échéances post-quantiques du projet de calendrier du NIST : signatures classiques interdites après 2035, jeux de paramètres de 112 bits comme RSA-2048 dépréciés après 2030.