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, скопированный дословно: сканер проверяет два файла из собственного набора тестовых фикстур. Каждая находка называет механизм, показывает исправление и ссылается на стандарт. Сам сканер выводит отчёт на английском.
$ 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.
Три уровня уверенности
Находки разделены по уровням, и уровень — это наше обязательство перед вами. Сканер, который в итоге отключают, ничего не стоит, поэтому провалить сборку могут только однозначные дефекты.
Этот шаблон — дефект независимо от окружающего кода.
Исправьте — и блокируйте по таким находкам сборки в CI: ненулевой код выхода дают только находки уровня confirmed.
Почти наверняка ошибка; всё решает окружающий код.
Прочитайте находку, затем исправьте код или отклоните её.
Стоит посмотреть: здесь срабатывает и вполне корректный код.
Считайте это вопросом, а не приговором.
15 правил
14 правил поиска дефектов и один сборщик данных для инвентаризации. ecdsa-scan rules выводит тот же список. Некоторые правила переводят находку на другой уровень в зависимости от контекста: закрытый ключ в test/fixtures/ получает уровень advisory, а не confirmed.
| Правило | Уверенность | Критичность | Что находит |
|---|---|---|---|
| jwt-verify-missing-algorithms | confirmed | высокая | jwt.verify / jwtVerify без algorithms:, decode в PyJWT без algorithms=, jwt.Parse без WithValidMethods, а также пустые списки или списки с «none». |
| jwt-alg-from-token | confirmed | высокая | Список алгоритмов для проверки, построенный из заголовка самого токена (algorithms: [header.alg], get_unverified_header). |
| jwt-decode-without-verification | suspected | высокая | jwt.decode, decodeJwt, jwtDecode, verify_signature: False в PyJWT; уровень повышается, если результат попадает в идентификаторы вроде role/user/permission. |
| insecure-nonce-source | confirmed | высокая | Math.random(), random в Python, math/rand рядом с материалом ключей или nonce; k, переданный вызывающим кодом, extraEntropy, deterministic: false. |
| hardcoded-private-key | confirmed | высокая | Блоки PEM с закрытым ключом в исходном коде; 64-символьные hex-литералы, присвоенные переменным с именами вроде privateKey/secret/signingKey. |
| key-file-outside-tests | suspected | высокая | Файлы .pem/.key/.p12/id_ecdsa с материалом закрытого ключа вне каталогов test/, fixtures/ и examples/. |
| unchecked-verification-result | suspected | высокая | Проверка, возвращающая булево значение (crypto.verify, ecdsa.VerifyASN1, …), вызвана отдельной инструкцией, а её результат отброшен. |
| weak-signature-hash | confirmed | высокая | SHA-1 или MD5 на пути подписи: createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA; дайджесты SHA-1 в других местах кода подписи. |
| tls-verification-disabled | confirmed | высокая | rejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False в HTTP-клиентах, ssl.CERT_NONE, InsecureSkipVerify: true. |
| signature-encoding | suspected | средняя | r и s, склеенные вручную без дополнения нулями до размера поля; crypto.sign/verify в Node без dsaEncoding в коде JWS. |
| non-constant-time-comparison | suspected | средняя | Подписи, MAC или дайджесты, которые сравниваются через ==, ===, .equals() или bytes.Equal, а не вспомогательной функцией с постоянным временем выполнения. |
| curve-mixing | advisory | средняя | Ключ P-256, созданный в коде для Ethereum/Bitcoin; secp256k1 и P-256 обрабатываются в одном модуле. |
| unvalidated-public-key-point | advisory | средняя | Открытые ключи, собранные из сырых координат x/y без проверки принадлежности точки кривой; самописная арифметика на кривой. |
| secp256k1-low-s | advisory | низкая | Подпись или проверка на 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.sarifBitbucket 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 года.