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 つのファイルをスキャンしたときの実際の出力を、そのまま掲載しています。各検出事項は、仕組みを示し、修正方法を提示し、該当する標準を引用します。スキャナー自体の出力は英語です。
$ 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 段階の信頼度
検出事項はレベル分けされており、そのレベルが利用者との約束です。ミュートされてしまうスキャナーには価値がありません。そのため、ビルドを失敗させられるのは明白な欠陥だけです。
周囲のコードに関係なく、このパターン自体が欠陥です。
修正し、CI のゲート条件にもしてください。0 以外の終了コードになるのは confirmed の検出事項だけです。
誤りの可能性が非常に高いものの、判断は周囲のコード次第です。
検出事項を読んだうえで、修正するか却下してください。
確認する価値があります。ただし、正当なコードもここに該当します。
判定ではなく、問いかけとして扱ってください。
15 のルール
14 の欠陥検出ルールと、1 つのインベントリ収集機能です。ecdsa-scan rules でも同じ一覧を表示できます。一部のルールは、文脈に応じて検出事項のレベルを変えます。たとえば、test/fixtures/ 配下の秘密鍵は confirmed ではなく advisory になります。
| ルール | 信頼度 | 重大度 | 検出内容 |
|---|---|---|---|
| jwt-verify-missing-algorithms | confirmed | 高 | algorithms: を指定しない jwt.verify / jwtVerify、algorithms= を指定しない PyJWT の decode、WithValidMethods を指定しない jwt.Parse。空のリストや "none" を含むリストも対象。 |
| jwt-alg-from-token | confirmed | 高 | トークン自身のヘッダーから組み立てた、検証用のアルゴリズムリスト(algorithms: [header.alg]、get_unverified_header)。 |
| jwt-decode-without-verification | suspected | 高 | jwt.decode、decodeJwt、jwtDecode、PyJWT の verify_signature: False。結果を role/user/permission などの識別子が使う場合はレベルを引き上げ。 |
| insecure-nonce-source | confirmed | 高 | 鍵や nonce の素材の近くで使われる Math.random()、Python の random、math/rand。呼び出し側が指定する k、extraEntropy、deterministic: false。 |
| hardcoded-private-key | confirmed | 高 | ソースコード内の PEM 形式の秘密鍵ブロック。privateKey/secret/signingKey のような名前に代入された 64 桁の 16 進リテラル。 |
| key-file-outside-tests | suspected | 高 | test/、fixtures/、examples/ ディレクトリの外にある、秘密鍵の素材を含む .pem/.key/.p12/id_ecdsa ファイル。 |
| 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、HTTP クライアントでの verify=False、ssl.CERT_NONE、InsecureSkipVerify: true。 |
| signature-encoding | suspected | 中 | フィールドサイズまでゼロパディングせずに、手作業で連結した r と s。JWS のコードで dsaEncoding を指定しない Node の crypto.sign/verify。 |
| non-constant-time-comparison | suspected | 中 | 定数時間で比較するヘルパーではなく、==、===、.equals()、bytes.Equal で比較している署名、MAC、ダイジェスト。 |
| curve-mixing | advisory | 中 | Ethereum/Bitcoin のコードで生成された P-256 鍵。同じモジュール内で扱われる secp256k1 と P-256。 |
| unvalidated-public-key-point | advisory | 中 | 曲線上の点かどうかを確認せずに、生の x/y 座標から組み立てた公開鍵。独自実装の曲線演算。 |
| secp256k1-low-s | advisory | 低 | 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.sarifBitbucket 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年を過ぎると非推奨になります。