Pular para o conteúdo
ecdsa.com

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.

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.

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.

confirmed

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.

suspected

Muito provavelmente errado; o código ao redor decide.

Leia o achado e depois corrija ou descarte.

advisory

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.

RegraConfiançaGravidadeO que ela encontra
jwt-verify-missing-algorithmsconfirmedaltajwt.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-tokenconfirmedaltaA 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-verificationsuspectedaltajwt.decode, decodeJwt, jwtDecode, verify_signature: False do PyJWT — o nível sobe quando identificadores como role/user/permission consomem o resultado.
insecure-nonce-sourceconfirmedaltaMath.random(), random do Python, math/rand perto de material de chave ou de nonce; k fornecido por quem chama, extraEntropy, deterministic: false.
hardcoded-private-keyconfirmedaltaBlocos 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-testssuspectedaltaArquivos .pem/.key/.p12/id_ecdsa com material de chave privada fora dos diretórios test/, fixtures/ e examples/.
unchecked-verification-resultsuspectedaltaVerificação que retorna booleano (crypto.verify, ecdsa.VerifyASN1, …) chamada como instrução isolada, com o resultado descartado.
weak-signature-hashconfirmedaltaSHA-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-disabledconfirmedaltarejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False em clientes HTTP, ssl.CERT_NONE, InsecureSkipVerify: true.
signature-encodingsuspectedmédiar 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-comparisonsuspectedmédiaAssinaturas, MACs ou digests comparados com ==, ===, .equals() ou bytes.Equal em vez de uma função auxiliar de tempo constante.
curve-mixingadvisorymédiaUma chave P-256 criada em código Ethereum/Bitcoin; secp256k1 e P-256 tratadas no mesmo módulo.
unvalidated-public-key-pointadvisorymédiaChaves 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-sadvisorybaixaAssinatura ou verificação com secp256k1 sem normalização low-S visível — uma questão de maleabilidade para código de blockchain.
crypto-inventoryn/an/aNã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.sarif

Bitbucket 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.sarif

Limitaçõ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.