ecdsa-scan · 免费 CLI · 零依赖 · Node 20+
找出代码中每一处签名的位置——以及其中的错误
机密扫描工具能找出已提交到仓库的密钥。ecdsa-scan 关注的是更上一层:暗藏错误的签名代码——验证 JWT 时未固定算法、在以太坊代码中生成 P-256 密钥、手工拼接 r‖s 签名却未做填充、验证结果的布尔值被直接丢弃。只读,无需构建步骤,无需配置。支持 JavaScript、TypeScript、Python 和 Go。
$ npx ecdsa-scan .无需安装,无需账号。只有出现 confirmed 级别的检测结果时才返回退出码 1——可直接用作 CI 门禁。全局安装:npm i -g ecdsa-scan
分级且附带解释的检测结果
每项检测结果都带有置信度级别、用浅显英文写成的机制说明、修正后的代码片段,以及相关 RFC 的链接。绝不只给出一条不加解释的模式匹配结果。
用于代码扫描的 SARIF
只需一个命令行选项,即可生成供 GitHub Code Scanning 使用的 SARIF 2.1.0: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 条缺陷规则和 1 个清单收集器。ecdsa-scan rules 会输出同样的列表。有些规则会根据上下文调整检测结果的级别——位于 test/fixtures/ 下的私钥属于 advisory,而不是 confirmed。
| 规则 | 置信度 | 严重程度 | 检测内容 |
|---|---|---|---|
| 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 个十六进制字符的字面量。 |
| 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 代码中调用 Node crypto.sign/verify 时未指定 dsaEncoding。 |
| non-constant-time-comparison | suspected | 中 | 用 ==、===、.equals() 或 bytes.Equal 而不是常量时间比较函数来比较签名、MAC 或摘要。 |
| curve-mixing | advisory | 中 | 在以太坊/比特币代码中创建 P-256 密钥;在同一模块中同时处理 secp256k1 和 P-256。 |
| unvalidated-public-key-point | advisory | 中 | 由原始 x/y 坐标构造公钥却不检查点是否在曲线上;手写的曲线运算。 |
| secp256k1-low-s | advisory | 低 | secp256k1 签名或验证中看不到 low-S 规范化——对区块链代码而言,这是延展性方面的问题。 |
| crypto-inventory | 不适用 | 不适用 | 不是缺陷规则:按文件收集密码库、算法、曲线和签名操作——即 CBOM 种子数据。 |
表中为默认置信度;单项检测结果可能因周围代码而被升级或降级。
CI 集成
只有存在 confirmed 级别的缺陷时,扫描器才以退出码 1 退出,因此可以接入任何流水线而不会带来噪音问题。SARIF 报告会把检测结果转换为 pull request 注释,每条注释都附带各自的修复方法和参考资料。
GitHub Actions
在推送和 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 级别的检测结果也以疑问的形式表述。
也会有漏报,而且漏报更糟。
任何间接的东西都看不到:在另一个文件中定义的包装函数、从配置中读取的算法、在运行时才决定的密钥类型、通过扫描器从未见过的库体现出来的缺陷。
弱随机性只能部分检测出来。
由隔着三个模块的自定义 PRNG 生成的有偏 nonce,看起来与正确的代码一模一样。nonce 的统计性问题要通过分析签名来发现,而不是分析源代码。
没有跨文件分析,没有数据流分析,也没有污点追踪。
每个文件都单独判断。这是付出的代价,换来的是在任何仓库、任何状态下都能立即运行,无需安装其依赖,也无需编译任何东西。
对注释和字面量的屏蔽是启发式的。
不寻常的格式——词法分析器误读的正则表达式、包含实际逻辑的模板字面量——可能会让检测结果被漏掉。
通过扫描不等于做过审计。
扫描器检查的是有没有某些众所周知的错误,而不是你的密码学实现是否正确。请把输出当作一份按优先级排好的阅读清单,交给人工审查者。