本文へスキップ
ecdsa.com

ecdsa-scan・無料の CLI・依存関係ゼロ・Node 20 以上

コード内で署名しているすべての箇所と、そこにある誤りを見つける

シークレットスキャナーは、コミットされた鍵を見つけます。ecdsa-scan が探すのは、その一段上の層、つまり見落としやすい誤りを含む署名コードです。たとえば、アルゴリズムを固定せずに検証している JWT、Ethereum のコードで生成された P-256 鍵、パディングなしで手作業で組み立てた r‖s 署名、真偽値の結果が捨てられている検証などです。読み取り専用で、ビルド手順も設定も不要です。JavaScript、TypeScript、Python、Go に対応しています。

$ npx ecdsa-scan .

インストールもアカウントも不要です。終了コード 1 を返すのは confirmed の検出事項がある場合だけなので、そのまま CI のゲートに使えます。グローバルインストール:npm i -g ecdsa-scan

レベル分けと解説付きの検出事項

すべての検出事項には、信頼度のレベル、平易な英語による仕組みの説明、修正済みのスニペット、関連する RFC へのリンクが付きます。パターンに一致しただけの結果をそのまま示すことはありません。

コードスキャン向けの SARIF

フラグ 1 つで GitHub Code Scanning 用の SARIF 2.1.0 を出力します。confirmed → error、suspected → warning、advisory → note に対応付けられ、各アノテーションには修正方法が埋め込まれます。

CBOM のシードデータを無料で

JSON レポートには、どの暗号ライブラリ、アルゴリズム、曲線、署名操作がどのファイルに登場するかが一覧化されます。これがポスト量子暗号への移行マップの素材になります。

スキャンの実行例

ecdsa-scan 0.1.0 が自身のテストフィクスチャから 2 つのファイルをスキャンしたときの実際の出力を、そのまま掲載しています。各検出事項は、仕組みを示し、修正方法を提示し、該当する標準を引用します。スキャナー自体の出力は英語です。

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.

3 段階の信頼度

検出事項はレベル分けされており、そのレベルが利用者との約束です。ミュートされてしまうスキャナーには価値がありません。そのため、ビルドを失敗させられるのは明白な欠陥だけです。

confirmed

周囲のコードに関係なく、このパターン自体が欠陥です。

修正し、CI のゲート条件にもしてください。0 以外の終了コードになるのは confirmed の検出事項だけです。

suspected

誤りの可能性が非常に高いものの、判断は周囲のコード次第です。

検出事項を読んだうえで、修正するか却下してください。

advisory

確認する価値があります。ただし、正当なコードもここに該当します。

判定ではなく、問いかけとして扱ってください。

15 のルール

14 の欠陥検出ルールと、1 つのインベントリ収集機能です。ecdsa-scan rules でも同じ一覧を表示できます。一部のルールは、文脈に応じて検出事項のレベルを変えます。たとえば、test/fixtures/ 配下の秘密鍵は confirmed ではなく advisory になります。

ルール信頼度重大度検出内容
jwt-verify-missing-algorithmsconfirmed高algorithms: を指定しない jwt.verify / jwtVerify、algorithms= を指定しない PyJWT の decode、WithValidMethods を指定しない jwt.Parse。空のリストや "none" を含むリストも対象。
jwt-alg-from-tokenconfirmed高トークン自身のヘッダーから組み立てた、検証用のアルゴリズムリスト(algorithms: [header.alg]、get_unverified_header)。
jwt-decode-without-verificationsuspected高jwt.decode、decodeJwt、jwtDecode、PyJWT の verify_signature: False。結果を role/user/permission などの識別子が使う場合はレベルを引き上げ。
insecure-nonce-sourceconfirmed高鍵や nonce の素材の近くで使われる Math.random()、Python の random、math/rand。呼び出し側が指定する k、extraEntropy、deterministic: false。
hardcoded-private-keyconfirmed高ソースコード内の PEM 形式の秘密鍵ブロック。privateKey/secret/signingKey のような名前に代入された 64 桁の 16 進リテラル。
key-file-outside-testssuspected高test/、fixtures/、examples/ ディレクトリの外にある、秘密鍵の素材を含む .pem/.key/.p12/id_ecdsa ファイル。
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、HTTP クライアントでの verify=False、ssl.CERT_NONE、InsecureSkipVerify: true。
signature-encodingsuspected中フィールドサイズまでゼロパディングせずに、手作業で連結した r と s。JWS のコードで dsaEncoding を指定しない Node の crypto.sign/verify。
non-constant-time-comparisonsuspected中定数時間で比較するヘルパーではなく、==、===、.equals()、bytes.Equal で比較している署名、MAC、ダイジェスト。
curve-mixingadvisory中Ethereum/Bitcoin のコードで生成された P-256 鍵。同じモジュール内で扱われる secp256k1 と P-256。
unvalidated-public-key-pointadvisory中曲線上の点かどうかを確認せずに、生の x/y 座標から組み立てた公開鍵。独自実装の曲線演算。
secp256k1-low-sadvisory低low-S 正規化が見当たらない secp256k1 の署名または検証。ブロックチェーンのコードでは展性の観点から確認が必要。
crypto-inventory該当なし該当なし欠陥検出ルールではなく、ファイルごとに暗号ライブラリ、アルゴリズム、曲線、署名操作を収集(CBOM のシードデータ)。

表示しているのはデフォルトの信頼度です。個々の検出事項は、周囲のコードに応じてレベルが上がることも下がることもあります。

CI との連携

スキャナーが終了コード 1 を返すのは confirmed の欠陥がある場合だけなので、ノイズに悩まされることなく、どのパイプラインにも組み込めます。SARIF レポートは検出事項をプルリクエストのアノテーションに変換し、各アノテーションには修正方法と参照先が付きます。

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

confirmed の検出事項があれば PR のステップを失敗させ、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 の検出事項は問いかけの形で書かれています。

  • 検出漏れも起こり、そちらのほうが深刻です。

    間接的なものはすべて見えません。別のファイルで定義されたラッパー関数、設定から読み込まれるアルゴリズム、実行時に決まる鍵の種類、スキャナーが知らないライブラリを通じて現れる欠陥などです。

  • 弱い乱数は一部しか検出できません。

    3 つ離れたモジュールにある独自の PRNG が生成した偏りのある nonce は、正しいコードとまったく同じに見えます。nonce の統計的な問題は、ソースコードではなく署名を調べることで見つかります。

  • ファイルをまたぐ解析、データフロー解析、テイント追跡は行いません。

    各ファイルは個別に判定されます。これは、依存関係のインストールやコンパイルを一切せずに、どのような状態のどのリポジトリでも即座に実行できることの代償です。

  • コメントとリテラルのマスキングはヒューリスティックです。

    字句解析器が誤読する正規表現や、実際のロジックを含むテンプレートリテラルなど、通常とは異なる書き方によって検出事項が隠れることがあります。

  • スキャンに合格しても、監査を受けたことにはなりません。

    スキャナーが確認するのは、よく知られた特定の誤りがないことであり、暗号の実装が正しいことではありません。出力は、人間のレビュー担当者が優先順に読むためのリストとして扱ってください。

ロードマップ

スキャナーは第一歩。次は署名インベントリのプラットフォーム

スキャンでわかるのは、現時点でコードのどこが署名しているかです。開発中のプラットフォームは、そのマップを常に最新に保ちます。リポジトリから鍵、環境までを 1 つのインベントリにまとめ、どの鍵が存在し、何に署名し、どのアルゴリズムを使っているか、そして NIST のポスト量子暗号の期限案までに何を移行しなければならないかを把握できます。期限案では、従来型の署名は 2035年を過ぎると禁止され、RSA-2048 のような 112 ビットのパラメーターセットは 2030年を過ぎると非推奨になります。