ecdsa-scan · CLI gratuita · zero dependências · Node 20+
Encontre todos os pontos em que seu código assina — e o que está errado ali.
Scanners de segredos encontram chaves que foram parar em commits. O ecdsa-scan procura na camada acima disso: código de assinatura que está sutilmente errado — um JWT verificado sem fixar o algoritmo, uma chave P-256 gerada em código Ethereum, uma assinatura r‖s montada à mão sem padding, uma verificação cujo resultado booleano é descartado. Somente leitura, sem etapa de build, sem configuração. JavaScript, TypeScript, Python e Go.
$ npx ecdsa-scan .Sem instalação, sem conta. Código de saída 1 só em achados de nível confirmed — pronto para servir de gate no CI. Instalação global: npm i -g ecdsa-scan
Achados classificados e explicados
Cada achado traz um nível de confiança, uma explicação do mecanismo em inglês simples, um trecho de código corrigido e um link para a RFC relevante. Nada de correspondências de padrão sem contexto.
SARIF para code scanning
Uma única flag gera SARIF 2.1.0 para o GitHub Code Scanning: confirmed → error, suspected → warning, advisory → note — com a correção embutida em cada anotação.
Uma semente de CBOM, de graça
O relatório JSON faz o inventário de quais bibliotecas de criptografia, algoritmos, curvas e operações de assinatura aparecem em quais arquivos — a matéria-prima para o mapa da migração pós-quântica.
Como é uma varredura
Saída real do ecdsa-scan 0.1.0, capturada sem alterações, ao escanear dois arquivos da própria suíte de fixtures de teste da ferramenta. Cada achado nomeia o mecanismo, mostra a correção e cita o padrão. O próprio scanner gera os relatórios em inglês.
$ 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.
Três níveis de confiança
Os achados são classificados, e a classificação é o contrato com você. Um scanner que as pessoas silenciam não vale nada, então só defeitos inequívocos podem reprovar um build.
O padrão é um defeito independentemente do código ao redor.
Corrija — e faça o CI falhar com ele: só os achados confirmed terminam com código de saída diferente de zero.
Muito provavelmente errado; o código ao redor decide.
Leia o achado e depois corrija ou descarte.
Vale uma olhada — código legítimo também cai aqui.
Trate como uma pergunta, não como um veredito.
As 15 regras
Quatorze regras de defeito e um coletor de inventário. O ecdsa-scan rules exibe a mesma lista. Algumas regras movem um achado entre níveis conforme o contexto — uma chave privada em test/fixtures/ fica no nível advisory, não confirmed.
| Regra | Confiança | Gravidade | O que ela encontra |
|---|---|---|---|
| jwt-verify-missing-algorithms | confirmed | alta | jwt.verify / jwtVerify sem algorithms:, decode do PyJWT sem algorithms=, jwt.Parse sem WithValidMethods — além de listas de algoritmos vazias ou com "none". |
| jwt-alg-from-token | confirmed | alta | A lista de algoritmos de verificação montada a partir do próprio cabeçalho do token (algorithms: [header.alg], get_unverified_header). |
| jwt-decode-without-verification | suspected | alta | jwt.decode, decodeJwt, jwtDecode, verify_signature: False do PyJWT — o nível sobe quando identificadores como role/user/permission consomem o resultado. |
| insecure-nonce-source | confirmed | alta | Math.random(), random do Python, math/rand perto de material de chave ou de nonce; k fornecido por quem chama, extraEntropy, deterministic: false. |
| hardcoded-private-key | confirmed | alta | Blocos PEM de chave privada no código-fonte; literais de 64 dígitos hexadecimais atribuídos a nomes no estilo privateKey/secret/signingKey. |
| key-file-outside-tests | suspected | alta | Arquivos .pem/.key/.p12/id_ecdsa com material de chave privada fora dos diretórios test/, fixtures/ e examples/. |
| unchecked-verification-result | suspected | alta | Verificação que retorna booleano (crypto.verify, ecdsa.VerifyASN1, …) chamada como instrução isolada, com o resultado descartado. |
| weak-signature-hash | confirmed | alta | SHA-1 ou MD5 em um caminho de assinatura: createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA; digests SHA-1 em outros pontos do código de assinatura. |
| tls-verification-disabled | confirmed | alta | rejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False em clientes HTTP, ssl.CERT_NONE, InsecureSkipVerify: true. |
| signature-encoding | suspected | média | r e s concatenados à mão sem padding com zeros até o tamanho do campo; crypto.sign/verify do Node sem dsaEncoding em código JWS. |
| non-constant-time-comparison | suspected | média | Assinaturas, MACs ou digests comparados com ==, ===, .equals() ou bytes.Equal em vez de uma função auxiliar de tempo constante. |
| curve-mixing | advisory | média | Uma chave P-256 criada em código Ethereum/Bitcoin; secp256k1 e P-256 tratadas no mesmo módulo. |
| unvalidated-public-key-point | advisory | média | Chaves públicas montadas a partir de coordenadas x/y brutas sem verificar se o ponto está na curva; aritmética de curva implementada à mão. |
| secp256k1-low-s | advisory | baixa | Assinatura ou verificação com secp256k1 sem normalização low-S visível — uma questão de maleabilidade para código de blockchain. |
| crypto-inventory | n/a | n/a | Não é uma regra de defeito: coleta as bibliotecas de criptografia, os algoritmos, as curvas e as operações de assinatura de cada arquivo — a semente do CBOM. |
A tabela mostra a confiança padrão; cada achado pode subir ou descer de nível conforme o código ao redor.
Integração com CI
O scanner sai com código 1 só quando existe um defeito confirmed, então ele se encaixa em qualquer pipeline sem gerar ruído. O relatório SARIF transforma os achados em anotações nos pull requests, cada uma com a própria correção e referência.
GitHub Actions
Faça a varredura em push e PR e publique os achados no 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
Reprove a etapa do PR em achados confirmed e guarde o SARIF como artefato.
# 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.sarifLimitações — leia isto
Isto é correspondência de padrões sobre texto, não análise de programa. O scanner é franco sobre o que isso oferece e o que isso custa:
Falsos positivos acontecem.
Um módulo que oferece suporte legítimo a várias curvas dispara a regra curve-mixing; uma ferramenta de depuração chama jwt.decode de forma legítima. É por isso que todo achado traz um nível de confiança, que só os achados confirmed reprovam o build e que os achados advisory são formulados como perguntas.
Falsos negativos acontecem, e são piores.
Tudo o que é indireto fica invisível: uma função wrapper definida em outro arquivo, um algoritmo lido da configuração, um tipo de chave decidido em tempo de execução, um defeito expresso por meio de uma biblioteca que o scanner não conhece.
Aleatoriedade fraca só é detectável em parte.
Um nonce enviesado, produzido por um PRNG personalizado a três módulos de distância, parece exatamente código correto. Problemas estatísticos de nonce são encontrados examinando assinaturas, não o código-fonte.
Sem análise entre arquivos, sem fluxo de dados, sem rastreamento de taint.
Cada arquivo é avaliado isoladamente. Esse é o preço de rodar instantaneamente em qualquer repositório, em qualquer estado, sem instalar as dependências dele nem compilar nada.
O mascaramento de comentários e literais é heurístico.
Uma formatação incomum — uma regex que o lexer interpreta errado, um template literal com lógica real — pode esconder um achado.
Passar na varredura não é uma auditoria.
O scanner verifica a ausência de certos erros bem conhecidos, não se a sua criptografia está correta. Trate a saída como uma lista de leitura priorizada para um revisor humano.
Roadmap
O scanner é o primeiro passo — a plataforma de inventário de assinaturas está chegando.
Uma varredura mostra onde seu código assina hoje. A plataforma que estamos construindo mantém esse mapa vivo: um único inventário do repositório à chave e ao ambiente — quais chaves existem, o que elas assinam, quais algoritmos usam e o que precisa migrar antes dos prazos pós-quânticos do rascunho do NIST: assinaturas clássicas proibidas depois de 2035 e conjuntos de parâmetros de 112 bits, como o RSA-2048, obsoletos depois de 2030.