ecdsa-scan · kostenloses CLI · keine Abhängigkeiten · Node 20+
Finde jede Stelle, an der dein Code signiert – und was dort falsch ist.
Secret-Scanner finden eingecheckte Schlüssel. ecdsa-scan sucht eine Ebene darüber: nach Signaturcode, der subtil falsch ist – ein JWT, das ohne Algorithmus-Pinning geprüft wird, ein P-256-Schlüssel, der in Ethereum-Code erzeugt wird, eine r‖s-Signatur, die von Hand ohne Padding zusammengesetzt wird, eine Prüfung, deren boolesches Ergebnis verworfen wird. Nur lesend, kein Build-Schritt, keine Konfiguration. JavaScript, TypeScript, Python und Go.
$ npx ecdsa-scan .Keine Installation, kein Konto. Exit-Code 1 nur bei Befunden der Stufe confirmed – bereit als Gate für die CI. Globale Installation: npm i -g ecdsa-scan
Eingestufte, erklärte Befunde
Jeder Befund hat eine Konfidenzstufe, eine Erklärung des Mechanismus in einfachem Englisch, ein korrigiertes Snippet und einen Link zum passenden RFC. Keine bloßen Mustertreffer.
SARIF für Code Scanning
Ein einziges Flag erzeugt SARIF 2.1.0 für GitHub Code Scanning: confirmed → error, suspected → warning, advisory → note – mit der Korrektur direkt in jeder Annotation.
Grundstock für eine CBOM, gratis dazu
Der JSON-Bericht erfasst, welche Kryptobibliotheken, Algorithmen, Kurven und Signiervorgänge in welchen Dateien vorkommen – das Rohmaterial für die Karte deiner Post-Quanten-Migration.
So sieht ein Scan aus
Echte Ausgabe von ecdsa-scan 0.1.0, wörtlich mitgeschnitten beim Scan zweier Dateien aus seiner eigenen Test-Fixture-Suite. Jeder Befund nennt den Mechanismus, zeigt die Korrektur und verweist auf den Standard. Der Scanner selbst berichtet auf Englisch.
$ 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.
Drei Konfidenzstufen
Befunde werden eingestuft, und die Stufe ist unsere Zusage an dich. Ein Scanner, der stummgeschaltet wird, ist nichts wert – deshalb können nur eindeutige Mängel einen Build scheitern lassen.
Das Muster ist unabhängig vom umgebenden Code ein Mangel.
Behebe den Mangel – und lass die CI daran scheitern: Nur Befunde der Stufe confirmed führen zu einem Exit-Code ungleich null.
Sehr wahrscheinlich falsch; der umgebende Code entscheidet.
Lies den Befund, dann behebe ihn oder verwirf ihn.
Einen Blick wert – auch legitimer Code löst hier einen Treffer aus.
Behandle ihn als Frage, nicht als Urteil.
Die 15 Regeln
Vierzehn Regeln für Mängel und ein Inventar-Sammler. ecdsa-scan rules gibt dieselbe Liste aus. Manche Regeln verschieben einen Befund je nach Kontext zwischen den Stufen – ein privater Schlüssel unter test/fixtures/ ist advisory, nicht confirmed.
| Regel | Konfidenz | Schweregrad | Was sie findet |
|---|---|---|---|
| jwt-verify-missing-algorithms | confirmed | hoch | jwt.verify / jwtVerify ohne algorithms:, decode von PyJWT ohne algorithms=, jwt.Parse ohne WithValidMethods – außerdem leere Listen oder Listen mit „none“. |
| jwt-alg-from-token | confirmed | hoch | Liste der Algorithmen für die Prüfung, gebildet aus dem eigenen Header des Tokens (algorithms: [header.alg], get_unverified_header). |
| jwt-decode-without-verification | suspected | hoch | jwt.decode, decodeJwt, jwtDecode, PyJWT mit verify_signature: False – hochgestuft, wenn Bezeichner wie role/user/permission das Ergebnis verwenden. |
| insecure-nonce-source | confirmed | hoch | Math.random(), random aus Python, math/rand in der Nähe von Schlüssel- oder Nonce-Material; vom Aufrufer übergebenes k, extraEntropy, deterministic: false. |
| hardcoded-private-key | confirmed | hoch | PEM-Blöcke mit privaten Schlüsseln im Quellcode; Literale aus 64 Hex-Zeichen, die Namen wie privateKey/secret/signingKey zugewiesen werden. |
| key-file-outside-tests | suspected | hoch | Dateien .pem/.key/.p12/id_ecdsa mit Material privater Schlüssel außerhalb der Verzeichnisse test/, fixtures/ und examples/. |
| unchecked-verification-result | suspected | hoch | Prüfungen mit booleschem Ergebnis (crypto.verify, ecdsa.VerifyASN1, …), als bloße Anweisung aufgerufen – das Ergebnis wird verworfen. |
| weak-signature-hash | confirmed | hoch | SHA-1 oder MD5 in einem Signierpfad: createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA; SHA-1-Digests an anderen Stellen im Signiercode. |
| tls-verification-disabled | confirmed | hoch | rejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False bei HTTP-Clients, ssl.CERT_NONE, InsecureSkipVerify: true. |
| signature-encoding | suspected | mittel | r und s von Hand verkettet, ohne Zero-Padding auf die Feldgröße der Kurve; crypto.sign/verify in Node ohne dsaEncoding in JWS-Code. |
| non-constant-time-comparison | suspected | mittel | Signaturen, MACs oder Digests, die mit ==, ===, .equals() oder bytes.Equal statt mit einer zeitkonstanten Hilfsfunktion verglichen werden. |
| curve-mixing | advisory | mittel | Ein P-256-Schlüssel, der in Ethereum- oder Bitcoin-Code erzeugt wird; secp256k1 und P-256 im selben Modul verarbeitet. |
| unvalidated-public-key-point | advisory | mittel | Öffentliche Schlüssel, die aus rohen x/y-Koordinaten gebaut werden, ohne zu prüfen, ob der Punkt auf der Kurve liegt; selbst geschriebene Kurvenarithmetik. |
| secp256k1-low-s | advisory | niedrig | Signieren oder Prüfen mit secp256k1 ohne sichtbare Low-S-Normalisierung – eine Frage der Malleability für Blockchain-Code. |
| crypto-inventory | entfällt | entfällt | Keine Mängelregel: sammelt pro Datei die Kryptobibliotheken, Algorithmen, Kurven und Signiervorgänge – der Grundstock für die CBOM. |
Angezeigt ist die Standardkonfidenz; einzelne Befunde können durch ihren umgebenden Code hoch- oder herabgestuft werden.
CI-Integration
Der Scanner endet nur dann mit Exit-Code 1, wenn ein Mangel der Stufe confirmed vorliegt – so passt er in jede Pipeline, ohne dass Fehlalarme zum Problem werden. Der SARIF-Bericht macht aus Befunden Pull-Request-Annotationen, jede mit eigener Korrektur und eigenem Verweis.
GitHub Actions
Bei Push und PR scannen, Befunde in GitHub Code Scanning veröffentlichen.
# .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
Den PR-Schritt bei Befunden der Stufe confirmed scheitern lassen, das SARIF als Artefakt aufbewahren.
# 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.sarifGrenzen – lies das
Das ist Mustererkennung auf Textebene, keine Programmanalyse. Was das bringt und was es kostet, steht hier ohne Beschönigung:
Falsch-Positive kommen vor.
Ein Modul, das berechtigterweise mehrere Kurven unterstützt, schlägt bei curve-mixing an; ein Debugging-Tool ruft berechtigterweise jwt.decode auf. Deshalb hat jeder Befund eine Konfidenzstufe, deshalb lassen nur Befunde der Stufe confirmed den Build scheitern, und deshalb sind Befunde der Stufe advisory als Fragen formuliert.
Falsch-Negative kommen vor, und sie sind schlimmer.
Alles Indirekte bleibt unsichtbar: eine Wrapper-Funktion, die in einer anderen Datei definiert ist, ein Algorithmus, der aus der Konfiguration gelesen wird, ein Schlüsseltyp, der erst zur Laufzeit feststeht, ein Mangel, der über eine Bibliothek entsteht, von der der Scanner nie gehört hat.
Schwacher Zufall ist nur teilweise erkennbar.
Eine verzerrte Nonce aus einem eigenen PRNG drei Module weiter sieht genau wie korrekter Code aus. Statistische Probleme mit Nonces zeigen sich in den Signaturen, nicht im Quellcode.
Keine dateiübergreifende Analyse, kein Datenfluss, kein Taint-Tracking.
Jede Datei wird für sich beurteilt. Das ist der Preis dafür, dass der Scanner sofort auf jedem Repository läuft, in jedem Zustand, ohne dessen Abhängigkeiten zu installieren oder etwas zu kompilieren.
Das Ausblenden von Kommentaren und Literalen ist heuristisch.
Ungewöhnliche Formatierung – ein regulärer Ausdruck, den der Lexer falsch liest, ein Template-Literal mit echter Logik – kann einen Befund verdecken.
Ein bestandener Scan ist kein Audit.
Der Scanner prüft, ob bestimmte bekannte Fehler fehlen – nicht, ob deine Kryptografie korrekt ist. Behandle die Ausgabe als priorisierte Leseliste für ein Review durch einen Menschen.
Roadmap
Der Scanner ist der erste Schritt – die Plattform für das Signatur-Inventar kommt.
Ein Scan zeigt dir, wo dein Code heute signiert. Die Plattform, die wir bauen, hält diese Karte aktuell: ein Inventar vom Repository über den Schlüssel bis zur Umgebung – welche Schlüssel es gibt, was sie signieren, welche Algorithmen sie nutzen und was migriert werden muss, bevor die Post-Quanten-Fristen aus dem NIST-Entwurf greifen: klassische Signaturen nach 2035 nicht mehr zulässig, 112-Bit-Parametersätze wie RSA-2048 nach 2030 als veraltet eingestuft.