Перейти к содержимому
ecdsa.com

ecdsa-scan · бесплатный CLI · без зависимостей · Node 20+

Найдите все места, где ваш код создаёт подписи, — и то, что в них не так.

Сканеры секретов находят ключи, попавшие в коммиты. ecdsa-scan ищет уровнем выше: код подписей с неочевидными ошибками — JWT, проверенный без закрепления алгоритма, ключ P-256, сгенерированный в коде для Ethereum, подпись r‖s, собранная вручную без дополнения, проверка с отброшенным булевым результатом. Только чтение, без шага сборки и без настройки. JavaScript, TypeScript, Python и Go.

$ npx ecdsa-scan .

Без установки и без аккаунта. Код выхода 1 — только при находках уровня confirmed, так что сканер можно сразу сделать обязательной проверкой в CI. Глобальная установка: npm i -g ecdsa-scan

Находки с уровнем и объяснением

У каждой находки есть уровень уверенности, понятное объяснение механизма на английском языке, исправленный фрагмент кода и ссылка на соответствующий RFC. Никаких голых совпадений с шаблоном.

SARIF для сканирования кода

Один флаг — и сканер выдаёт SARIF 2.1.0 для GitHub Code Scanning: confirmed → error, suspected → warning, advisory → note, а исправление встроено в каждую аннотацию.

Основа для CBOM в придачу

JSON-отчёт перечисляет, какие криптографические библиотеки, алгоритмы, кривые и операции подписи встречаются в каких файлах, — исходный материал для карты миграции на постквантовые алгоритмы.

Как выглядит сканирование

Реальный вывод ecdsa-scan 0.1.0, скопированный дословно: сканер проверяет два файла из собственного набора тестовых фикстур. Каждая находка называет механизм, показывает исправление и ссылается на стандарт. Сам сканер выводит отчёт на английском.

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.

Три уровня уверенности

Находки разделены по уровням, и уровень — это наше обязательство перед вами. Сканер, который в итоге отключают, ничего не стоит, поэтому провалить сборку могут только однозначные дефекты.

confirmed

Этот шаблон — дефект независимо от окружающего кода.

Исправьте — и блокируйте по таким находкам сборки в CI: ненулевой код выхода дают только находки уровня confirmed.

suspected

Почти наверняка ошибка; всё решает окружающий код.

Прочитайте находку, затем исправьте код или отклоните её.

advisory

Стоит посмотреть: здесь срабатывает и вполне корректный код.

Считайте это вопросом, а не приговором.

15 правил

14 правил поиска дефектов и один сборщик данных для инвентаризации. ecdsa-scan rules выводит тот же список. Некоторые правила переводят находку на другой уровень в зависимости от контекста: закрытый ключ в test/fixtures/ получает уровень advisory, а не confirmed.

ПравилоУверенностьКритичностьЧто находит
jwt-verify-missing-algorithmsconfirmedвысокаяjwt.verify / jwtVerify без algorithms:, decode в PyJWT без algorithms=, jwt.Parse без WithValidMethods, а также пустые списки или списки с «none».
jwt-alg-from-tokenconfirmedвысокаяСписок алгоритмов для проверки, построенный из заголовка самого токена (algorithms: [header.alg], get_unverified_header).
jwt-decode-without-verificationsuspectedвысокаяjwt.decode, decodeJwt, jwtDecode, verify_signature: False в PyJWT; уровень повышается, если результат попадает в идентификаторы вроде role/user/permission.
insecure-nonce-sourceconfirmedвысокаяMath.random(), random в Python, math/rand рядом с материалом ключей или nonce; k, переданный вызывающим кодом, extraEntropy, deterministic: false.
hardcoded-private-keyconfirmedвысокаяБлоки PEM с закрытым ключом в исходном коде; 64-символьные hex-литералы, присвоенные переменным с именами вроде privateKey/secret/signingKey.
key-file-outside-testssuspectedвысокаяФайлы .pem/.key/.p12/id_ecdsa с материалом закрытого ключа вне каталогов test/, fixtures/ и examples/.
unchecked-verification-resultsuspectedвысокаяПроверка, возвращающая булево значение (crypto.verify, ecdsa.VerifyASN1, …), вызвана отдельной инструкцией, а её результат отброшен.
weak-signature-hashconfirmedвысокаяSHA-1 или MD5 на пути подписи: createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA; дайджесты SHA-1 в других местах кода подписи.
tls-verification-disabledconfirmedвысокаяrejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False в HTTP-клиентах, ssl.CERT_NONE, InsecureSkipVerify: true.
signature-encodingsuspectedсредняяr и s, склеенные вручную без дополнения нулями до размера поля; crypto.sign/verify в Node без dsaEncoding в коде JWS.
non-constant-time-comparisonsuspectedсредняяПодписи, MAC или дайджесты, которые сравниваются через ==, ===, .equals() или bytes.Equal, а не вспомогательной функцией с постоянным временем выполнения.
curve-mixingadvisoryсредняяКлюч P-256, созданный в коде для Ethereum/Bitcoin; secp256k1 и P-256 обрабатываются в одном модуле.
unvalidated-public-key-pointadvisoryсредняяОткрытые ключи, собранные из сырых координат x/y без проверки принадлежности точки кривой; самописная арифметика на кривой.
secp256k1-low-sadvisoryнизкаяПодпись или проверка на secp256k1 без видимой нормализации low-S — вопрос пластичности подписей для блокчейн-кода.
crypto-inventoryнеприменимонеприменимоНе правило поиска дефектов: собирает по каждому файлу криптографические библиотеки, алгоритмы, кривые и операции подписи — основу для CBOM.

Указана уверенность по умолчанию; окружающий код может повысить или понизить уровень отдельной находки.

Интеграция с CI

Сканер завершается с кодом 1, только если найден дефект уровня confirmed, поэтому он встраивается в любой пайплайн без лишнего шума. Отчёт SARIF превращает находки в аннотации к pull request, и в каждой есть своё исправление и ссылка на источник.

GitHub Actions

Сканирование при push и в PR, публикация находок в 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.sarif

Bitbucket Pipelines

Провал шага PR при находках уровня confirmed, сохранение SARIF как артефакта.

# 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

Ограничения — обязательно прочитайте

Это поиск шаблонов в тексте, а не анализ программы. Честно о том, что это даёт и чего стоит:

  • Ложные срабатывания случаются.

    Модуль, который вполне законно поддерживает несколько кривых, попадает под правило curve-mixing; инструмент отладки вполне законно вызывает jwt.decode. Поэтому у каждой находки есть уровень уверенности, сборку проваливают только находки уровня confirmed, а находки уровня advisory сформулированы как вопросы.

  • Пропуски тоже случаются, и они хуже.

    Всё косвенное остаётся невидимым: функция-обёртка, определённая в другом файле, алгоритм, считанный из конфигурации, тип ключа, выбираемый во время выполнения, дефект, выраженный через библиотеку, о которой сканер никогда не слышал.

  • Слабую случайность удаётся обнаружить лишь отчасти.

    Смещённый nonce из самописного ГПСЧ, который находится тремя модулями дальше, выглядит в точности как корректный код. Статистические проблемы с nonce находят, анализируя подписи, а не исходный код.

  • Ни анализа связей между файлами, ни анализа потоков данных, ни taint-анализа.

    Каждый файл оценивается сам по себе. Это цена того, что сканер мгновенно запускается на любом репозитории в любом состоянии, без установки зависимостей проекта и без какой-либо компиляции.

  • Маскирование комментариев и литералов эвристическое.

    Необычное форматирование — регулярное выражение, которое лексер разбирает неверно, или шаблонная строка с настоящей логикой — может скрыть находку.

  • Чистый результат сканирования — ещё не аудит.

    Сканер проверяет, что некоторых хорошо известных ошибок нет, а не то, что ваша криптография корректна. Считайте его вывод списком мест, которые человеку-ревьюеру стоит прочитать в порядке приоритета.

Планы развития

Сканер — первый шаг: скоро появится платформа инвентаризации подписей.

Сканирование показывает, где ваш код подписывает данные сегодня. Платформа, которую мы строим, поддерживает эту карту в актуальном состоянии: единая инвентаризация от репозитория к ключу и окружению — какие ключи существуют, что они подписывают, какие алгоритмы используют и что нужно перевести на новые алгоритмы до постквантовых сроков из проекта NIST: классические подписи запрещены после 2035 года, 112-битные наборы параметров вроде RSA-2048 устаревают после 2030 года.