Saltar al contenido
ecdsa.com

ecdsa-scan · CLI gratuita · sin dependencias · Node 20+

Encuentra todos los puntos en los que firma tu código, y lo que falla en ellos.

Los escáneres de secretos encuentran claves incluidas en commits. ecdsa-scan busca en la capa superior: código de firma que está sutilmente mal —un JWT verificado sin fijar el algoritmo, una clave P-256 generada en código de Ethereum, una firma r‖s construida a mano sin relleno, una verificación cuyo resultado booleano se descarta—. Solo lectura, sin paso de compilación, sin configuración. JavaScript, TypeScript, Python y Go.

$ npx ecdsa-scan .

Sin instalación, sin cuenta. Código de salida 1 solo con hallazgos confirmed: listo para usarlo como control en CI. Instalación global: npm i -g ecdsa-scan

Hallazgos clasificados y explicados

Cada hallazgo incluye un nivel de confianza, una explicación del mecanismo en inglés sencillo, un fragmento de código corregido y un enlace al RFC correspondiente. Nada de simples coincidencias de patrones.

SARIF para el análisis de código

Una sola opción genera SARIF 2.1.0 para GitHub Code Scanning: confirmed → error, suspected → warning, advisory → note, con la corrección incluida en cada anotación.

La semilla de tu CBOM, gratis

El informe JSON inventaría qué bibliotecas criptográficas, algoritmos, curvas y operaciones de firma aparecen en qué archivos: la materia prima del mapa de migración poscuántica.

Así es un escaneo

Salida real, capturada literalmente de ecdsa-scan 0.1.0 al escanear dos archivos de su propio conjunto de fixtures de prueba. Cada hallazgo nombra el mecanismo, muestra la corrección y cita el estándar. El escáner en sí informa en 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.

Tres niveles de confianza

Los hallazgos se clasifican por niveles, y el nivel es nuestro compromiso contigo. Un escáner que la gente silencia no vale nada, así que solo los defectos inequívocos pueden hacer fallar una compilación.

confirmed

El patrón es un defecto independientemente del código que lo rodee.

Corrígelo y usa estos hallazgos como control en CI: solo los de nivel confirmed terminan con un código de salida distinto de cero.

suspected

Muy probablemente es un error; el código que lo rodea decide.

Lee el hallazgo y después corrígelo o descártalo.

advisory

Merece un vistazo: aquí también coincide código legítimo.

Tómalo como una pregunta, no como un veredicto.

Las 15 reglas

Catorce reglas de defectos y un recolector de inventario. ecdsa-scan rules muestra la misma lista. Algunas reglas cambian el nivel de un hallazgo según el contexto: una clave privada en test/fixtures/ recibe el nivel advisory, no confirmed.

ReglaConfianzaGravedadQué detecta
jwt-verify-missing-algorithmsconfirmedaltajwt.verify / jwtVerify sin algorithms:, decode de PyJWT sin algorithms=, jwt.Parse sin WithValidMethods, además de listas vacías o con "none".
jwt-alg-from-tokenconfirmedaltaLa lista de algoritmos de verificación construida a partir del propio encabezado del token (algorithms: [header.alg], get_unverified_header).
jwt-decode-without-verificationsuspectedaltajwt.decode, decodeJwt, jwtDecode, PyJWT verify_signature: False; el hallazgo sube de nivel cuando el resultado lo consumen identificadores de tipo role/user/permission.
insecure-nonce-sourceconfirmedaltaMath.random(), random de Python o math/rand junto a material de clave o de nonce; k proporcionado por quien llama, extraEntropy, deterministic: false.
hardcoded-private-keyconfirmedaltaBloques PEM de clave privada en el código fuente; literales de 64 caracteres hexadecimales asignados a nombres del tipo privateKey/secret/signingKey.
key-file-outside-testssuspectedaltaArchivos .pem/.key/.p12/id_ecdsa con material de clave privada fuera de los directorios test/, fixtures/ y examples/.
unchecked-verification-resultsuspectedaltaFunciones de verificación que devuelven un booleano (crypto.verify, ecdsa.VerifyASN1, …) llamadas como una sentencia suelta, con el resultado descartado.
weak-signature-hashconfirmedaltaSHA-1 o MD5 en una ruta de firma: createSign("sha1"), hashes.SHA1(), x509.SHA1WithRSA; resúmenes SHA-1 en otras partes del código de firma.
tls-verification-disabledconfirmedaltarejectUnauthorized: false, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False en clientes HTTP, ssl.CERT_NONE, InsecureSkipVerify: true.
signature-encodingsuspectedmediar y s concatenados a mano sin relleno con ceros hasta el tamaño del campo; crypto.sign/verify de Node sin dsaEncoding en código JWS.
non-constant-time-comparisonsuspectedmediaFirmas, MAC o resúmenes comparados con ==, ===, .equals() o bytes.Equal en lugar de con una función auxiliar de tiempo constante.
curve-mixingadvisorymediaUna clave P-256 creada en código de Ethereum/Bitcoin; secp256k1 y P-256 manejadas en el mismo módulo.
unvalidated-public-key-pointadvisorymediaClaves públicas construidas a partir de coordenadas x/y en bruto, sin comprobar que el punto pertenece a la curva; aritmética de curvas implementada a mano.
secp256k1-low-sadvisorybajaFirma o verificación secp256k1 sin una normalización low-S visible: una cuestión de maleabilidad en el código de blockchain.
crypto-inventoryn/an/aNo es una regla de defectos: recopila las bibliotecas criptográficas, los algoritmos, las curvas y las operaciones de firma de cada archivo; es la semilla del CBOM.

Se muestra la confianza predeterminada; el código que rodea a cada hallazgo puede subir o bajar su nivel.

Integración con CI

El escáner termina con código 1 solo cuando hay un defecto de nivel confirmed, así que encaja en cualquier pipeline sin generar ruido. El informe SARIF convierte los hallazgos en anotaciones de pull request, cada una con su propia corrección y su referencia.

GitHub Actions

Escanea en cada push y PR y publica los hallazgos en 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

Haz fallar el paso del PR con hallazgos de nivel confirmed y conserva el SARIF como artefacto.

# 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

Limitaciones: lee esto

Esto es búsqueda de patrones sobre texto, no análisis de programas. Y deja claro lo que eso aporta y lo que cuesta:

  • Hay falsos positivos.

    Un módulo que admite legítimamente varias curvas coincide con curve-mixing; una herramienta de depuración llama legítimamente a jwt.decode. Por eso cada hallazgo lleva un nivel de confianza, solo los hallazgos de nivel confirmed hacen fallar la compilación y los de nivel advisory se formulan como preguntas.

  • Hay falsos negativos, y son peores.

    Todo lo indirecto es invisible: una función envoltorio definida en otro archivo, un algoritmo leído de la configuración, un tipo de clave decidido en tiempo de ejecución, un defecto expresado a través de una biblioteca que el escáner no conoce.

  • La aleatoriedad débil solo se puede detectar en parte.

    Un nonce sesgado producido por un PRNG propio a tres módulos de distancia parece exactamente código correcto. Los problemas estadísticos de los nonces se detectan examinando las firmas, no el código fuente.

  • Sin análisis entre archivos, sin flujo de datos ni seguimiento de datos contaminados (taint tracking).

    Cada archivo se evalúa por separado. Es el precio de ejecutarse al instante en cualquier repositorio, en cualquier estado, sin instalar sus dependencias ni compilar nada.

  • El enmascaramiento de comentarios y literales es heurístico.

    Un formato inusual —una regex que el analizador léxico interpreta mal, una plantilla literal con lógica real— puede ocultar un hallazgo.

  • Pasar el escaneo no equivale a una auditoría.

    El escáner comprueba que no haya ciertos errores bien conocidos, no que tu criptografía sea correcta. Trata la salida como una lista de lectura priorizada para un revisor humano.

Hoja de ruta

El escáner es el primer paso: la plataforma de inventario de firma está en camino.

Un escaneo te dice dónde firma tu código hoy. La plataforma que estamos construyendo mantiene ese mapa al día con un único inventario que va del repositorio a la clave y al entorno: qué claves existen, qué firman, qué algoritmos usan y qué tiene que migrar antes de los plazos poscuánticos del borrador del NIST (firmas clásicas no permitidas después de 2035; conjuntos de parámetros de 112 bits como RSA-2048, obsoletos después de 2030).