Zum Inhalt springen
ecdsa.com

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.

demo-app — npx ecdsa-scan .
$ 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.

confirmed

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.

suspected

Sehr wahrscheinlich falsch; der umgebende Code entscheidet.

Lies den Befund, dann behebe ihn oder verwirf ihn.

advisory

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.

RegelKonfidenzSchweregradWas sie findet
jwt-verify-missing-algorithmsconfirmedhochjwt.verify / jwtVerify ohne algorithms:, decode von PyJWT ohne algorithms=, jwt.Parse ohne WithValidMethods – außerdem leere Listen oder Listen mit „none“.
jwt-alg-from-tokenconfirmedhochListe der Algorithmen für die Prüfung, gebildet aus dem eigenen Header des Tokens (algorithms: [header.alg], get_unverified_header).
jwt-decode-without-verificationsuspectedhochjwt.decode, decodeJwt, jwtDecode, PyJWT mit verify_signature: False – hochgestuft, wenn Bezeichner wie role/user/permission das Ergebnis verwenden.
insecure-nonce-sourceconfirmedhochMath.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-keyconfirmedhochPEM-Blöcke mit privaten Schlüsseln im Quellcode; Literale aus 64 Hex-Zeichen, die Namen wie privateKey/secret/signingKey zugewiesen werden.
key-file-outside-testssuspectedhochDateien .pem/.key/.p12/id_ecdsa mit Material privater Schlüssel außerhalb der Verzeichnisse test/, fixtures/ und examples/.
unchecked-verification-resultsuspectedhochPrüfungen mit booleschem Ergebnis (crypto.verify, ecdsa.VerifyASN1, …), als bloße Anweisung aufgerufen – das Ergebnis wird verworfen.
weak-signature-hashconfirmedhochSHA-1 oder MD5 in einem Signierpfad: createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA; SHA-1-Digests an anderen Stellen im Signiercode.
tls-verification-disabledconfirmedhochrejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False bei HTTP-Clients, ssl.CERT_NONE, InsecureSkipVerify: true.
signature-encodingsuspectedmittelr 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-comparisonsuspectedmittelSignaturen, MACs oder Digests, die mit ==, ===, .equals() oder bytes.Equal statt mit einer zeitkonstanten Hilfsfunktion verglichen werden.
curve-mixingadvisorymittelEin P-256-Schlüssel, der in Ethereum- oder Bitcoin-Code erzeugt wird; secp256k1 und P-256 im selben Modul verarbeitet.
unvalidated-public-key-pointadvisorymittelÖ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-sadvisoryniedrigSignieren oder Prüfen mit secp256k1 ohne sichtbare Low-S-Normalisierung – eine Frage der Malleability für Blockchain-Code.
crypto-inventoryentfälltentfälltKeine 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.sarif

Bitbucket 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.sarif

Grenzen – 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.