@quantakrypto/core 0.4.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -27
- package/dist/cbom-merge.d.ts.map +1 -1
- package/dist/cbom-merge.js +30 -8
- package/dist/cbom-merge.js.map +1 -1
- package/dist/cbom.d.ts +5 -0
- package/dist/cbom.d.ts.map +1 -1
- package/dist/cbom.js +132 -37
- package/dist/cbom.js.map +1 -1
- package/dist/comments.d.ts +2 -23
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +24 -5
- package/dist/comments.js.map +1 -1
- package/dist/crypto-agility.d.ts +158 -0
- package/dist/crypto-agility.d.ts.map +1 -0
- package/dist/crypto-agility.js +285 -0
- package/dist/crypto-agility.js.map +1 -0
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +180 -18
- package/dist/dependencies.js.map +1 -1
- package/dist/detect-utils.d.ts +55 -21
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +133 -7
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/age.d.ts +16 -0
- package/dist/detectors/age.d.ts.map +1 -0
- package/dist/detectors/age.js +40 -0
- package/dist/detectors/age.js.map +1 -0
- package/dist/detectors/ansible.d.ts +13 -0
- package/dist/detectors/ansible.d.ts.map +1 -0
- package/dist/detectors/ansible.js +108 -0
- package/dist/detectors/ansible.js.map +1 -0
- package/dist/detectors/bicep.d.ts +20 -0
- package/dist/detectors/bicep.d.ts.map +1 -0
- package/dist/detectors/bicep.js +74 -0
- package/dist/detectors/bicep.js.map +1 -0
- package/dist/detectors/c.d.ts.map +1 -1
- package/dist/detectors/c.js +30 -3
- package/dist/detectors/c.js.map +1 -1
- package/dist/detectors/cicd.d.ts.map +1 -1
- package/dist/detectors/cicd.js +18 -5
- package/dist/detectors/cicd.js.map +1 -1
- package/dist/detectors/cloud-kms.d.ts +8 -5
- package/dist/detectors/cloud-kms.d.ts.map +1 -1
- package/dist/detectors/cloud-kms.js +62 -20
- package/dist/detectors/cloud-kms.js.map +1 -1
- package/dist/detectors/cloudformation.d.ts +8 -0
- package/dist/detectors/cloudformation.d.ts.map +1 -1
- package/dist/detectors/cloudformation.js +41 -10
- package/dist/detectors/cloudformation.js.map +1 -1
- package/dist/detectors/codesign.d.ts +50 -0
- package/dist/detectors/codesign.d.ts.map +1 -0
- package/dist/detectors/codesign.js +143 -0
- package/dist/detectors/codesign.js.map +1 -0
- package/dist/detectors/csharp.d.ts.map +1 -1
- package/dist/detectors/csharp.js +13 -3
- package/dist/detectors/csharp.js.map +1 -1
- package/dist/detectors/dart.d.ts +35 -0
- package/dist/detectors/dart.d.ts.map +1 -0
- package/dist/detectors/dart.js +177 -0
- package/dist/detectors/dart.js.map +1 -0
- package/dist/detectors/database.d.ts +5 -3
- package/dist/detectors/database.d.ts.map +1 -1
- package/dist/detectors/database.js +45 -8
- package/dist/detectors/database.js.map +1 -1
- package/dist/detectors/dkim.d.ts +54 -0
- package/dist/detectors/dkim.d.ts.map +1 -0
- package/dist/detectors/dkim.js +92 -0
- package/dist/detectors/dkim.js.map +1 -0
- package/dist/detectors/dnssec.d.ts.map +1 -1
- package/dist/detectors/dnssec.js +19 -10
- package/dist/detectors/dnssec.js.map +1 -1
- package/dist/detectors/elixir.d.ts.map +1 -1
- package/dist/detectors/elixir.js +34 -2
- package/dist/detectors/elixir.js.map +1 -1
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +33 -3
- package/dist/detectors/java.js.map +1 -1
- package/dist/detectors/jose.d.ts.map +1 -1
- package/dist/detectors/jose.js +13 -3
- package/dist/detectors/jose.js.map +1 -1
- package/dist/detectors/jwk.d.ts +7 -7
- package/dist/detectors/jwk.d.ts.map +1 -1
- package/dist/detectors/jwk.js +44 -8
- package/dist/detectors/jwk.js.map +1 -1
- package/dist/detectors/k8s.d.ts.map +1 -1
- package/dist/detectors/k8s.js +5 -2
- package/dist/detectors/k8s.js.map +1 -1
- package/dist/detectors/keystore.d.ts +19 -0
- package/dist/detectors/keystore.d.ts.map +1 -0
- package/dist/detectors/keystore.js +76 -0
- package/dist/detectors/keystore.js.map +1 -0
- package/dist/detectors/mesh.d.ts +7 -8
- package/dist/detectors/mesh.d.ts.map +1 -1
- package/dist/detectors/mesh.js +17 -35
- package/dist/detectors/mesh.js.map +1 -1
- package/dist/detectors/messaging.d.ts +2 -2
- package/dist/detectors/messaging.d.ts.map +1 -1
- package/dist/detectors/messaging.js +19 -12
- package/dist/detectors/messaging.js.map +1 -1
- package/dist/detectors/objc.d.ts +41 -0
- package/dist/detectors/objc.d.ts.map +1 -0
- package/dist/detectors/objc.js +146 -0
- package/dist/detectors/objc.js.map +1 -0
- package/dist/detectors/openpgp.d.ts +20 -0
- package/dist/detectors/openpgp.d.ts.map +1 -0
- package/dist/detectors/openpgp.js +151 -0
- package/dist/detectors/openpgp.js.map +1 -0
- package/dist/detectors/pem.d.ts.map +1 -1
- package/dist/detectors/pem.js +32 -0
- package/dist/detectors/pem.js.map +1 -1
- package/dist/detectors/php.d.ts +2 -1
- package/dist/detectors/php.d.ts.map +1 -1
- package/dist/detectors/php.js +7 -3
- package/dist/detectors/php.js.map +1 -1
- package/dist/detectors/pkcs11.d.ts +17 -0
- package/dist/detectors/pkcs11.d.ts.map +1 -0
- package/dist/detectors/pkcs11.js +87 -0
- package/dist/detectors/pkcs11.js.map +1 -0
- package/dist/detectors/proxy.d.ts +52 -0
- package/dist/detectors/proxy.d.ts.map +1 -0
- package/dist/detectors/proxy.js +157 -0
- package/dist/detectors/proxy.js.map +1 -0
- package/dist/detectors/pulumi.d.ts +19 -0
- package/dist/detectors/pulumi.d.ts.map +1 -0
- package/dist/detectors/pulumi.js +80 -0
- package/dist/detectors/pulumi.js.map +1 -0
- package/dist/detectors/rust.d.ts.map +1 -1
- package/dist/detectors/rust.js +5 -0
- package/dist/detectors/rust.js.map +1 -1
- package/dist/detectors/secrets.d.ts +4 -1
- package/dist/detectors/secrets.d.ts.map +1 -1
- package/dist/detectors/secrets.js +5 -21
- package/dist/detectors/secrets.js.map +1 -1
- package/dist/detectors/solidity.d.ts +50 -0
- package/dist/detectors/solidity.d.ts.map +1 -0
- package/dist/detectors/solidity.js +108 -0
- package/dist/detectors/solidity.js.map +1 -0
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +105 -6
- package/dist/detectors/source.js.map +1 -1
- package/dist/detectors/spire.d.ts +49 -0
- package/dist/detectors/spire.d.ts.map +1 -0
- package/dist/detectors/spire.js +77 -0
- package/dist/detectors/spire.js.map +1 -0
- package/dist/detectors/ssh-ca.d.ts +72 -0
- package/dist/detectors/ssh-ca.d.ts.map +1 -0
- package/dist/detectors/ssh-ca.js +119 -0
- package/dist/detectors/ssh-ca.js.map +1 -0
- package/dist/detectors/supply-chain.d.ts +17 -0
- package/dist/detectors/supply-chain.d.ts.map +1 -0
- package/dist/detectors/supply-chain.js +89 -0
- package/dist/detectors/supply-chain.js.map +1 -0
- package/dist/detectors/swift.d.ts +22 -0
- package/dist/detectors/swift.d.ts.map +1 -0
- package/dist/detectors/swift.js +120 -0
- package/dist/detectors/swift.js.map +1 -0
- package/dist/detectors/terraform.d.ts.map +1 -1
- package/dist/detectors/terraform.js +35 -8
- package/dist/detectors/terraform.js.map +1 -1
- package/dist/detectors/vault.d.ts +15 -0
- package/dist/detectors/vault.d.ts.map +1 -0
- package/dist/detectors/vault.js +97 -0
- package/dist/detectors/vault.js.map +1 -0
- package/dist/detectors/vpn.d.ts +8 -8
- package/dist/detectors/vpn.d.ts.map +1 -1
- package/dist/detectors/vpn.js +12 -38
- package/dist/detectors/vpn.js.map +1 -1
- package/dist/detectors/weak-hash.d.ts +71 -0
- package/dist/detectors/weak-hash.d.ts.map +1 -0
- package/dist/detectors/weak-hash.js +116 -0
- package/dist/detectors/weak-hash.js.map +1 -0
- package/dist/detectors/webauthn.d.ts +61 -0
- package/dist/detectors/webauthn.d.ts.map +1 -0
- package/dist/detectors/webauthn.js +109 -0
- package/dist/detectors/webauthn.js.map +1 -0
- package/dist/detectors/xmldsig.d.ts +23 -0
- package/dist/detectors/xmldsig.d.ts.map +1 -0
- package/dist/detectors/xmldsig.js +84 -0
- package/dist/detectors/xmldsig.js.map +1 -0
- package/dist/evidence.d.ts +74 -4
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +71 -0
- package/dist/evidence.js.map +1 -1
- package/dist/hndl.d.ts +241 -0
- package/dist/hndl.d.ts.map +1 -0
- package/dist/hndl.js +752 -0
- package/dist/hndl.js.map +1 -0
- package/dist/index.d.ts +18 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -11
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +0 -2
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +15 -6
- package/dist/inventory.js.map +1 -1
- package/dist/parallel.d.ts.map +1 -1
- package/dist/parallel.js +22 -23
- package/dist/parallel.js.map +1 -1
- package/dist/registry.d.ts +9 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +51 -4
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +0 -3
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +1 -1
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/remediation.d.ts +10 -0
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +32 -0
- package/dist/remediation.js.map +1 -1
- package/dist/report.d.ts +17 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +106 -20
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js +5 -3
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +35 -6
- package/dist/scan.js.map +1 -1
- package/dist/standards-profiles.d.ts +58 -0
- package/dist/standards-profiles.d.ts.map +1 -0
- package/dist/standards-profiles.js +100 -0
- package/dist/standards-profiles.js.map +1 -0
- package/dist/types.d.ts +7 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/vex.d.ts +39 -0
- package/dist/vex.d.ts.map +1 -0
- package/dist/vex.js +87 -0
- package/dist/vex.js.map +1 -0
- package/dist/walk.d.ts +39 -4
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +62 -10
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom-merge.ts +38 -8
- package/src/cbom.ts +153 -39
- package/src/comments.ts +25 -6
- package/src/crypto-agility.ts +407 -0
- package/src/dependencies.ts +182 -16
- package/src/detect-utils.ts +133 -11
- package/src/detectors/age.ts +60 -0
- package/src/detectors/ansible.ts +140 -0
- package/src/detectors/bicep.ts +107 -0
- package/src/detectors/c.ts +31 -3
- package/src/detectors/cicd.ts +18 -5
- package/src/detectors/cloud-kms.ts +79 -25
- package/src/detectors/cloudformation.ts +45 -10
- package/src/detectors/codesign.ts +214 -0
- package/src/detectors/csharp.ts +13 -3
- package/src/detectors/dart.ts +237 -0
- package/src/detectors/database.ts +64 -11
- package/src/detectors/dkim.ts +172 -0
- package/src/detectors/dnssec.ts +19 -10
- package/src/detectors/elixir.ts +33 -2
- package/src/detectors/java.ts +37 -3
- package/src/detectors/jose.ts +19 -3
- package/src/detectors/jwk.ts +59 -17
- package/src/detectors/k8s.ts +5 -2
- package/src/detectors/keystore.ts +109 -0
- package/src/detectors/mesh.ts +21 -45
- package/src/detectors/messaging.ts +22 -14
- package/src/detectors/objc.ts +209 -0
- package/src/detectors/openpgp.ts +194 -0
- package/src/detectors/pem.ts +30 -0
- package/src/detectors/php.ts +9 -4
- package/src/detectors/pkcs11.ts +113 -0
- package/src/detectors/proxy.ts +236 -0
- package/src/detectors/pulumi.ts +106 -0
- package/src/detectors/rust.ts +5 -0
- package/src/detectors/secrets.ts +15 -26
- package/src/detectors/solidity.ts +176 -0
- package/src/detectors/source.ts +110 -5
- package/src/detectors/spire.ts +143 -0
- package/src/detectors/ssh-ca.ts +215 -0
- package/src/detectors/supply-chain.ts +117 -0
- package/src/detectors/swift.ts +149 -0
- package/src/detectors/terraform.ts +42 -8
- package/src/detectors/vault.ts +120 -0
- package/src/detectors/vpn.ts +20 -48
- package/src/detectors/weak-hash.ts +217 -0
- package/src/detectors/webauthn.ts +205 -0
- package/src/detectors/xmldsig.ts +115 -0
- package/src/evidence.ts +127 -4
- package/src/hndl.ts +1012 -0
- package/src/index.ts +93 -13
- package/src/inventory.ts +15 -7
- package/src/parallel.ts +21 -20
- package/src/registry.ts +51 -4
- package/src/remediate-pipeline.ts +1 -1
- package/src/remediation.ts +39 -0
- package/src/report.ts +120 -20
- package/src/scan-worker.ts +5 -3
- package/src/scan.ts +41 -6
- package/src/standards-profiles.ts +136 -0
- package/src/types.ts +18 -5
- package/src/version.ts +1 -1
- package/src/vex.ts +122 -0
- package/src/walk.ts +80 -12
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical-certificate TLS termination in standalone
|
|
3
|
+
* reverse-proxy / load-balancer configuration and in-code gRPC channel
|
|
4
|
+
* credentials.
|
|
5
|
+
*
|
|
6
|
+
* A standalone Envoy, Nginx, HAProxy, Traefik, or Caddy config that terminates
|
|
7
|
+
* TLS with an RSA/ECDSA leaf certificate and negotiates an ECDHE key exchange is
|
|
8
|
+
* the canonical harvest-now-decrypt-later (HNDL) surface: an attacker who
|
|
9
|
+
* records today's handshake + ciphertext can decrypt it retroactively once a
|
|
10
|
+
* cryptographically-relevant quantum computer (CRQC) can break the ephemeral
|
|
11
|
+
* (EC)DHE secret. The Kubernetes / service-mesh detectors (`k8s.ts`, `mesh.ts`)
|
|
12
|
+
* cover the CLUSTER-managed cert + mesh-mTLS surfaces; the STANDALONE proxy
|
|
13
|
+
* config FILES and the in-code gRPC credential constructors below are seen by
|
|
14
|
+
* neither them nor the language packs, so they are covered here.
|
|
15
|
+
*
|
|
16
|
+
* Because the config token itself does not name the cert's key algorithm (the
|
|
17
|
+
* key lives in a referenced PEM file, out of band), these findings use
|
|
18
|
+
* `algorithm: "unknown"` — the evidence is "a classical-cert TLS channel is
|
|
19
|
+
* configured HERE", not a specific family. They are `category: "key-exchange"`
|
|
20
|
+
* (the ECDHE handshake is the harvestable primitive) and `hndl: true`.
|
|
21
|
+
* Confidence is `medium`: a config-level indicator of a classical TLS channel,
|
|
22
|
+
* not a matched algorithm string.
|
|
23
|
+
*
|
|
24
|
+
* PRECISION / fast-reject: several of the matched tokens (`ssl_certificate`,
|
|
25
|
+
* `private_key:`, `certificates:`) are generic enough to appear in unrelated
|
|
26
|
+
* YAML/config, so `detect()` bails unless the file carries a distinctive
|
|
27
|
+
* proxy/gRPC marker (see `hasProxyMarker`). This keeps a random YAML that merely
|
|
28
|
+
* has a `private_key:` key, or a Traefik-shaped `certificates:` list that isn't
|
|
29
|
+
* a Traefik config, from firing. Comments are masked (YAML/nginx/HCL `#`, plus
|
|
30
|
+
* `//` and C-style `/* … *\/` blocks) so a commented-out directive can't fire.
|
|
31
|
+
*
|
|
32
|
+
* Covered surfaces:
|
|
33
|
+
* - Envoy — `common_tls_context:` / `DownstreamTlsContext` /
|
|
34
|
+
* `UpstreamTlsContext` with `tls_certificates:` carrying a
|
|
35
|
+
* `certificate_chain:` + `private_key:` (and the
|
|
36
|
+
* `envoy.transport_sockets.tls` transport socket).
|
|
37
|
+
* - Nginx — `ssl_certificate` + `ssl_certificate_key` directives.
|
|
38
|
+
* - HAProxy — `bind … ssl crt …` lines and `crt-store` sections.
|
|
39
|
+
* - Traefik — a `tls:` block's `certificates:` with `certFile:` + `keyFile:`
|
|
40
|
+
* (dynamic config) or a `certResolver` (ACME) reference.
|
|
41
|
+
* - gRPC — in-code TLS channel credentials: `grpc.ssl_channel_credentials(`
|
|
42
|
+
* (Python), `grpc.credentials.createSsl(` (Node),
|
|
43
|
+
* `TlsChannelCredentials` (Java), `credentials.NewTLS(` (Go).
|
|
44
|
+
*/
|
|
45
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
46
|
+
import {
|
|
47
|
+
DOC_EXTENSIONS,
|
|
48
|
+
eachMatch,
|
|
49
|
+
findingFromRule,
|
|
50
|
+
hasExtension,
|
|
51
|
+
maskBlockComments,
|
|
52
|
+
maskCommentLines,
|
|
53
|
+
} from "../detect-utils.js";
|
|
54
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
55
|
+
|
|
56
|
+
// --- Envoy: match ONLY the Envoy-DISTINCTIVE tokens (`common_tls_context:`, the
|
|
57
|
+
// `*TlsContext` type names, `tls_certificates:`, the `envoy.transport_sockets.tls`
|
|
58
|
+
// transport socket). The generic `certificate_chain:` / `private_key:` fields are
|
|
59
|
+
// deliberately NOT matched here — they appear in unrelated YAML and would
|
|
60
|
+
// cross-label a non-Envoy file as Envoy (audit L2). A distinctive token IS the
|
|
61
|
+
// evidence, so no separate marker gate is needed for this rule (audit L1).
|
|
62
|
+
const RE_ENVOY_TLS =
|
|
63
|
+
/\b(?:DownstreamTlsContext|UpstreamTlsContext|common_tls_context\s*:|tls_certificates\s*:|envoy\.transport_sockets\.tls)/g;
|
|
64
|
+
|
|
65
|
+
// --- Nginx: the classic `ssl_certificate` / `ssl_certificate_key` directive
|
|
66
|
+
// pair. `ssl_certificate_key` is matched first-class; `ssl_certificate` requires
|
|
67
|
+
// a following space so it doesn't also swallow `ssl_certificate_key` as a second
|
|
68
|
+
// hit on the same directive (kept as a single rule regardless).
|
|
69
|
+
const RE_NGINX_TLS = /\bssl_certificate(?:_key)?\s+\S/g;
|
|
70
|
+
|
|
71
|
+
// --- HAProxy: a `bind` line that terminates TLS (`ssl crt <path>`) or a
|
|
72
|
+
// `crt-store` section (HAProxy 2.8+ certificate store). `ssl` and `crt` may be
|
|
73
|
+
// separated by other bind args, so match them independently on a bind context.
|
|
74
|
+
const RE_HAPROXY_TLS =
|
|
75
|
+
/\bcrt-store\b|\bbind\b[^\n]*\bssl\b[^\n]*\bcrt\b|\bbind\b[^\n]*\bcrt\b[^\n]*\bssl\b/g;
|
|
76
|
+
|
|
77
|
+
// --- Traefik: dynamic-config TLS certificate entries (`certFile:` + `keyFile:`)
|
|
78
|
+
// or an ACME `certResolver` reference. `certFile`/`keyFile` are Traefik-specific
|
|
79
|
+
// enough (paired with the `certResolver`/`tls:` marker gate) to anchor on.
|
|
80
|
+
const RE_TRAEFIK_TLS = /\bcertResolver\b|\b(?:certFile|keyFile)\s*:/g;
|
|
81
|
+
|
|
82
|
+
// --- gRPC in-code TLS channel credentials across languages.
|
|
83
|
+
const RE_GRPC_TLS =
|
|
84
|
+
/grpc\.ssl_channel_credentials\s*\(|grpc\.credentials\.createSsl\s*\(|\bTlsChannelCredentials\b|\bcredentials\.NewTLS\s*\(/g;
|
|
85
|
+
|
|
86
|
+
const MESSAGE = (proxy: string): string =>
|
|
87
|
+
`${proxy} terminates TLS with a classical certificate + ECDHE key exchange; the recorded handshake is harvest-now-decrypt-later exposed — verify the cert key algorithm and plan a PQC-hybrid (X25519MLKEM768) TLS migration.`;
|
|
88
|
+
|
|
89
|
+
const REMEDIATION =
|
|
90
|
+
"Enable hybrid PQC key exchange (X25519MLKEM768) once the proxy/gRPC stack supports it; re-key certificates to PQC signatures (ML-DSA) when available.";
|
|
91
|
+
|
|
92
|
+
const RULE_ENVOY: RuleMeta = {
|
|
93
|
+
id: "proxy-envoy-tls",
|
|
94
|
+
title: "Envoy classical TLS termination",
|
|
95
|
+
description: "Envoy TLS context terminates TLS with a classical certificate + ECDHE key exchange",
|
|
96
|
+
category: "key-exchange",
|
|
97
|
+
severity: "medium",
|
|
98
|
+
confidence: "medium",
|
|
99
|
+
algorithm: "unknown",
|
|
100
|
+
hndl: true,
|
|
101
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
102
|
+
message: MESSAGE("Envoy"),
|
|
103
|
+
remediation: REMEDIATION,
|
|
104
|
+
};
|
|
105
|
+
const RULE_NGINX: RuleMeta = {
|
|
106
|
+
id: "proxy-nginx-tls",
|
|
107
|
+
title: "Nginx classical TLS termination",
|
|
108
|
+
description:
|
|
109
|
+
"Nginx ssl_certificate/ssl_certificate_key terminates TLS with a classical cert + ECDHE",
|
|
110
|
+
category: "key-exchange",
|
|
111
|
+
severity: "medium",
|
|
112
|
+
confidence: "medium",
|
|
113
|
+
algorithm: "unknown",
|
|
114
|
+
hndl: true,
|
|
115
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
116
|
+
message: MESSAGE("Nginx"),
|
|
117
|
+
remediation: REMEDIATION,
|
|
118
|
+
};
|
|
119
|
+
const RULE_HAPROXY: RuleMeta = {
|
|
120
|
+
id: "proxy-haproxy-tls",
|
|
121
|
+
title: "HAProxy classical TLS termination",
|
|
122
|
+
description: "HAProxy bind ssl crt / crt-store terminates TLS with a classical cert + ECDHE",
|
|
123
|
+
category: "key-exchange",
|
|
124
|
+
severity: "medium",
|
|
125
|
+
confidence: "medium",
|
|
126
|
+
algorithm: "unknown",
|
|
127
|
+
hndl: true,
|
|
128
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
129
|
+
message: MESSAGE("HAProxy"),
|
|
130
|
+
remediation: REMEDIATION,
|
|
131
|
+
};
|
|
132
|
+
const RULE_TRAEFIK: RuleMeta = {
|
|
133
|
+
id: "proxy-traefik-tls",
|
|
134
|
+
title: "Traefik classical TLS termination",
|
|
135
|
+
description:
|
|
136
|
+
"Traefik tls certificates (certFile/keyFile) or certResolver terminate TLS with a classical cert + ECDHE",
|
|
137
|
+
category: "key-exchange",
|
|
138
|
+
severity: "medium",
|
|
139
|
+
confidence: "medium",
|
|
140
|
+
algorithm: "unknown",
|
|
141
|
+
hndl: true,
|
|
142
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
143
|
+
message: MESSAGE("Traefik"),
|
|
144
|
+
remediation: REMEDIATION,
|
|
145
|
+
};
|
|
146
|
+
const RULE_GRPC: RuleMeta = {
|
|
147
|
+
id: "grpc-tls-credentials",
|
|
148
|
+
title: "gRPC classical TLS channel credentials",
|
|
149
|
+
description: "gRPC channel established with classical TLS credentials (RSA/ECDSA cert + ECDHE)",
|
|
150
|
+
category: "key-exchange",
|
|
151
|
+
severity: "medium",
|
|
152
|
+
confidence: "medium",
|
|
153
|
+
algorithm: "unknown",
|
|
154
|
+
hndl: true,
|
|
155
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
156
|
+
message: MESSAGE("gRPC"),
|
|
157
|
+
remediation: REMEDIATION,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* PERF fast-reject only — a superset of substrings that appear in every rule's
|
|
162
|
+
* matchable input, so it can never drop a file a rule would flag. Correctness is
|
|
163
|
+
* in the per-rule regexes (each is distinctive enough to self-gate); this list
|
|
164
|
+
* exists purely to skip the regex work on files with no proxy/gRPC token at all.
|
|
165
|
+
*/
|
|
166
|
+
const PROXY_FAST_REJECT: readonly string[] = [
|
|
167
|
+
// Envoy
|
|
168
|
+
"tls_context",
|
|
169
|
+
"TlsContext",
|
|
170
|
+
"tls_certificates",
|
|
171
|
+
"transport_sockets.tls",
|
|
172
|
+
// Nginx
|
|
173
|
+
"ssl_certificate",
|
|
174
|
+
// HAProxy (`crt-store` and `bind … ssl … crt` both contain "crt")
|
|
175
|
+
"crt",
|
|
176
|
+
// Traefik
|
|
177
|
+
"certResolver",
|
|
178
|
+
"certFile",
|
|
179
|
+
"keyFile",
|
|
180
|
+
// gRPC
|
|
181
|
+
"ssl_channel_credentials",
|
|
182
|
+
"createSsl",
|
|
183
|
+
"TlsChannelCredentials",
|
|
184
|
+
"credentials.NewTLS",
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
interface ProxyRule {
|
|
188
|
+
meta: RuleMeta;
|
|
189
|
+
re: RegExp;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const PROXY_RULES: readonly ProxyRule[] = [
|
|
193
|
+
{ meta: RULE_ENVOY, re: RE_ENVOY_TLS },
|
|
194
|
+
{ meta: RULE_NGINX, re: RE_NGINX_TLS },
|
|
195
|
+
{ meta: RULE_HAPROXY, re: RE_HAPROXY_TLS },
|
|
196
|
+
{ meta: RULE_TRAEFIK, re: RE_TRAEFIK_TLS },
|
|
197
|
+
{ meta: RULE_GRPC, re: RE_GRPC_TLS },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Detects classical-cert TLS termination in standalone reverse-proxy /
|
|
202
|
+
* load-balancer config files and in-code gRPC channel credentials. See the
|
|
203
|
+
* module docstring for the covered surfaces and the fast-reject rationale.
|
|
204
|
+
*/
|
|
205
|
+
export const proxyDetector: Detector = {
|
|
206
|
+
id: "proxy-tls-crypto",
|
|
207
|
+
description:
|
|
208
|
+
"Classical-cert TLS termination in reverse-proxy / load-balancer config (Envoy, Nginx, HAProxy, Traefik) and gRPC channel credentials",
|
|
209
|
+
scope: "config",
|
|
210
|
+
language: "any",
|
|
211
|
+
rules: PROXY_RULES.map((r) => r.meta),
|
|
212
|
+
// Config + source files are all in scope (gRPC creds live in code); only prose
|
|
213
|
+
// documentation is excluded, so a README mentioning `ssl_certificate` in a
|
|
214
|
+
// sentence can't fire (the marker gate already handles most of this).
|
|
215
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
216
|
+
detect({ file, content }): Finding[] {
|
|
217
|
+
// Mask comments first so a directive that only appears inside a commented-out
|
|
218
|
+
// block cannot fire.
|
|
219
|
+
const scan = maskCommentLines(maskBlockComments(content), ["#", "//"]);
|
|
220
|
+
// Cheap fast-reject: skip files with none of the distinctive tokens. This is a
|
|
221
|
+
// perf gate only — each rule's regex is distinctive enough to SELF-GATE (a
|
|
222
|
+
// separate too-narrow marker list previously dropped canonical HAProxy/Traefik
|
|
223
|
+
// configs entirely — audit H1/H2), so correctness lives in the per-rule regexes.
|
|
224
|
+
if (!PROXY_FAST_REJECT.some((mk) => scan.includes(mk))) return [];
|
|
225
|
+
|
|
226
|
+
const findings: Finding[] = [];
|
|
227
|
+
for (const { meta, re } of PROXY_RULES) {
|
|
228
|
+
eachMatch(re, scan, (m) =>
|
|
229
|
+
findings.push(
|
|
230
|
+
findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return findings;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IaC detector: classical asymmetric keys provisioned by Pulumi's `tls` provider
|
|
3
|
+
* (`@pulumi/tls` / `pulumi_tls` / `pulumi-tls`), across the languages Pulumi programs
|
|
4
|
+
* are written in (TS/JS, Python, Go). A `tls.PrivateKey` resource mints real key
|
|
5
|
+
* material at deploy time — the IaC surface, like Terraform's `tls_private_key`, but
|
|
6
|
+
* expressed as SDK calls in a general-purpose source file rather than HCL.
|
|
7
|
+
*
|
|
8
|
+
* Detection is gated to files that actually use the pulumi-tls provider (a
|
|
9
|
+
* `@pulumi/tls` / `pulumi_tls` / `pulumi-tls` import or a `tls.PrivateKey` /
|
|
10
|
+
* `tls.NewPrivateKey` construction), then classifies each `algorithm` value:
|
|
11
|
+
* - `"RSA"` → RSA key (kem, HNDL)
|
|
12
|
+
* - `"ECDSA"` → EC signing key (signature)
|
|
13
|
+
* - `"ED25519"` → Ed25519 signing key (signature)
|
|
14
|
+
* Comment/string suppression is handled centrally for the host languages.
|
|
15
|
+
*/
|
|
16
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
17
|
+
import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
18
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
19
|
+
|
|
20
|
+
const PULUMI_EXTENSIONS: readonly string[] = [".ts", ".js", ".mjs", ".cjs", ".py", ".go"];
|
|
21
|
+
|
|
22
|
+
// The `algorithm` value of a pulumi-tls PrivateKey, in the TS/Python (`algorithm:
|
|
23
|
+
// "RSA"` / `algorithm="RSA"`) and Go (`Algorithm: pulumi.String("RSA")`) spellings.
|
|
24
|
+
const RE_PULUMI_TLS_ALG =
|
|
25
|
+
/\b[Aa]lgorithm\s*[:=]\s*(?:pulumi\.String\(\s*)?["'](RSA|ECDSA|ED25519)["']/g;
|
|
26
|
+
|
|
27
|
+
/** True when the file actually uses the pulumi-tls provider (gate against FPs). */
|
|
28
|
+
function usesPulumiTls(content: string): boolean {
|
|
29
|
+
return (
|
|
30
|
+
content.includes("@pulumi/tls") ||
|
|
31
|
+
content.includes("pulumi_tls") ||
|
|
32
|
+
content.includes("pulumi-tls") ||
|
|
33
|
+
content.includes("tls.PrivateKey") ||
|
|
34
|
+
content.includes("tls.NewPrivateKey")
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const RULE_PULUMI_RSA: RuleMeta = {
|
|
39
|
+
id: "pulumi-tls-rsa",
|
|
40
|
+
title: "Pulumi tls.PrivateKey RSA key",
|
|
41
|
+
description: 'Pulumi tls.PrivateKey with algorithm "RSA"',
|
|
42
|
+
category: "kem",
|
|
43
|
+
severity: "high",
|
|
44
|
+
confidence: "high",
|
|
45
|
+
algorithm: "RSA",
|
|
46
|
+
hndl: true,
|
|
47
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
48
|
+
message: "Pulumi provisions a classical RSA key (tls.PrivateKey), which is not quantum-safe.",
|
|
49
|
+
remediation: "Plan migration to PQC (ML-KEM-768 for encryption, ML-DSA-65 for signatures).",
|
|
50
|
+
};
|
|
51
|
+
const RULE_PULUMI_ECDSA: RuleMeta = {
|
|
52
|
+
id: "pulumi-tls-ecdsa",
|
|
53
|
+
title: "Pulumi tls.PrivateKey ECDSA key",
|
|
54
|
+
description: 'Pulumi tls.PrivateKey with algorithm "ECDSA"',
|
|
55
|
+
category: "signature",
|
|
56
|
+
severity: "high",
|
|
57
|
+
confidence: "high",
|
|
58
|
+
algorithm: "ECDSA",
|
|
59
|
+
hndl: false,
|
|
60
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
61
|
+
message:
|
|
62
|
+
"Pulumi provisions a classical ECDSA key (tls.PrivateKey), forgeable by a quantum attacker.",
|
|
63
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
64
|
+
};
|
|
65
|
+
const RULE_PULUMI_ED25519: RuleMeta = {
|
|
66
|
+
id: "pulumi-tls-ed25519",
|
|
67
|
+
title: "Pulumi tls.PrivateKey Ed25519 key",
|
|
68
|
+
description: 'Pulumi tls.PrivateKey with algorithm "ED25519"',
|
|
69
|
+
category: "signature",
|
|
70
|
+
severity: "medium",
|
|
71
|
+
confidence: "high",
|
|
72
|
+
algorithm: "EdDSA",
|
|
73
|
+
hndl: false,
|
|
74
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
75
|
+
message:
|
|
76
|
+
"Pulumi provisions a classical Ed25519 key (tls.PrivateKey), forgeable by a quantum attacker.",
|
|
77
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const RULE_BY_ALG: Record<string, RuleMeta> = {
|
|
81
|
+
RSA: RULE_PULUMI_RSA,
|
|
82
|
+
ECDSA: RULE_PULUMI_ECDSA,
|
|
83
|
+
ED25519: RULE_PULUMI_ED25519,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Detects classical asymmetric keys provisioned by Pulumi's tls provider. */
|
|
87
|
+
export const pulumiDetector: Detector = {
|
|
88
|
+
id: "pulumi-crypto",
|
|
89
|
+
description: "Classical asymmetric keys provisioned by Pulumi's tls provider (IaC)",
|
|
90
|
+
scope: "config",
|
|
91
|
+
language: "any",
|
|
92
|
+
rules: [RULE_PULUMI_RSA, RULE_PULUMI_ECDSA, RULE_PULUMI_ED25519],
|
|
93
|
+
appliesTo: (f) => hasExtension(f, PULUMI_EXTENSIONS),
|
|
94
|
+
detect({ file, content }): Finding[] {
|
|
95
|
+
if (!usesPulumiTls(content)) return [];
|
|
96
|
+
const findings: Finding[] = [];
|
|
97
|
+
eachMatch(RE_PULUMI_TLS_ALG, content, (m) => {
|
|
98
|
+
const rule = RULE_BY_ALG[m[1]];
|
|
99
|
+
if (!rule) return;
|
|
100
|
+
findings.push(
|
|
101
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
return findings;
|
|
105
|
+
},
|
|
106
|
+
};
|
package/src/detectors/rust.ts
CHANGED
|
@@ -27,6 +27,10 @@ const RE_RUST_ECDH = /\becdh::EphemeralSecret\b|\bagreement::ECDH_P(?:256|384)\b
|
|
|
27
27
|
const RE_RUST_ED25519 = /\bed25519_dalek::(?:SigningKey|Keypair|SecretKey)\b|\bEd25519KeyPair::/g;
|
|
28
28
|
// X25519 key agreement (dalek).
|
|
29
29
|
const RE_RUST_X25519 = /\bx25519_dalek::(?:EphemeralSecret|StaticSecret)\b/g;
|
|
30
|
+
// X448 key agreement (`x448` crate). The qualified `x448::Secret` path — covers the
|
|
31
|
+
// plain `use x448::Secret;` / fully-qualified form the alias mechanism (which only
|
|
32
|
+
// fires on a RENAMED `use … as`) misses.
|
|
33
|
+
const RE_RUST_X448 = /\bx448::Secret\b/g;
|
|
30
34
|
// openssl crate bindings — the `Type::generate(` / `Dh::` call forms, distinct
|
|
31
35
|
// from the ring / RustCrypto path syntax above (audit F5/F9 false-negatives).
|
|
32
36
|
const RE_RUST_OPENSSL_RSA = /\bRsa::generate\s*\(/g;
|
|
@@ -346,6 +350,7 @@ export const rustDetector: Detector = {
|
|
|
346
350
|
add(RE_RUST_ECDH, RULE_RUST_ECDH);
|
|
347
351
|
add(RE_RUST_ED25519, RULE_RUST_ED25519);
|
|
348
352
|
add(RE_RUST_X25519, RULE_RUST_X25519);
|
|
353
|
+
add(RE_RUST_X448, RULE_RUST_X448);
|
|
349
354
|
add(RE_RUST_OPENSSL_RSA, RULE_RUST_OPENSSL_RSA);
|
|
350
355
|
add(RE_RUST_OPENSSL_EC, RULE_RUST_OPENSSL_EC);
|
|
351
356
|
add(RE_RUST_OPENSSL_DSA, RULE_RUST_OPENSSL_DSA);
|
package/src/detectors/secrets.ts
CHANGED
|
@@ -8,15 +8,24 @@
|
|
|
8
8
|
* Covered:
|
|
9
9
|
* - Mozilla SOPS / age recipients: `age1…` bech32 public keys (X25519 key
|
|
10
10
|
* agreement wrapping the data key).
|
|
11
|
-
* - PGP-encrypted payloads: `-----BEGIN PGP MESSAGE-----` (RSA/ElGamal ESK).
|
|
12
11
|
* - Bitnami Sealed Secrets: `kind: SealedSecret` (controller wraps with RSA-OAEP).
|
|
13
12
|
*
|
|
13
|
+
* Armored PGP blocks (`-----BEGIN PGP MESSAGE-----` and PGP private keys) are owned
|
|
14
|
+
* by the PEM detector (`pem-pgp-message` / `pem-pgp-private-key`), so they are not
|
|
15
|
+
* duplicated here.
|
|
16
|
+
*
|
|
14
17
|
* Symmetric-only schemes (ansible-vault AES, age with a scrypt passphrase) are
|
|
15
18
|
* intentionally NOT flagged: a strong symmetric key is only Grover-weakened, not
|
|
16
19
|
* broken, so it is out of scope for a *classical-asymmetric* readiness signal.
|
|
17
20
|
*/
|
|
18
21
|
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
19
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
DOC_EXTENSIONS,
|
|
24
|
+
eachMatch,
|
|
25
|
+
findingFromRule,
|
|
26
|
+
hasExtension,
|
|
27
|
+
maskCommentLines,
|
|
28
|
+
} from "../detect-utils.js";
|
|
20
29
|
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
21
30
|
|
|
22
31
|
interface SecretRule {
|
|
@@ -44,24 +53,6 @@ const SECRET_RULES: SecretRule[] = [
|
|
|
44
53
|
"Track a post-quantum age recipient / KMS (ML-KEM) and re-encrypt; rotate any secret whose ciphertext has left your control.",
|
|
45
54
|
},
|
|
46
55
|
},
|
|
47
|
-
{
|
|
48
|
-
re: /-----BEGIN PGP MESSAGE-----/g,
|
|
49
|
-
meta: {
|
|
50
|
-
id: "secrets-pgp-message",
|
|
51
|
-
title: "PGP-encrypted secret (RSA/ElGamal)",
|
|
52
|
-
description: "A PGP MESSAGE block: the session key is wrapped with classical RSA/ElGamal",
|
|
53
|
-
category: "kem",
|
|
54
|
-
severity: "high",
|
|
55
|
-
confidence: "high",
|
|
56
|
-
algorithm: "RSA",
|
|
57
|
-
hndl: true,
|
|
58
|
-
cwe: CWE_BROKEN_CRYPTO,
|
|
59
|
-
message:
|
|
60
|
-
"A PGP-encrypted secret whose session key is wrapped with classical RSA/ElGamal; harvest-now-decrypt-later exposed.",
|
|
61
|
-
remediation:
|
|
62
|
-
"Re-encrypt with a post-quantum KEM (ML-KEM-768) once available; rotate the underlying secret.",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
56
|
{
|
|
66
57
|
re: /\bkind:\s*["']?SealedSecret\b/g,
|
|
67
58
|
meta: {
|
|
@@ -94,16 +85,14 @@ export const secretsDetector: Detector = {
|
|
|
94
85
|
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
95
86
|
detect({ file, content }): Finding[] {
|
|
96
87
|
// Fast reject: none of the distinctive markers present.
|
|
97
|
-
if (
|
|
98
|
-
!content.includes("age1") &&
|
|
99
|
-
!content.includes("BEGIN PGP MESSAGE") &&
|
|
100
|
-
!content.includes("SealedSecret")
|
|
101
|
-
) {
|
|
88
|
+
if (!content.includes("age1") && !content.includes("SealedSecret")) {
|
|
102
89
|
return [];
|
|
103
90
|
}
|
|
91
|
+
// A commented `# kind: SealedSecret` / `# age1…` is not an active setting.
|
|
92
|
+
const scan = maskCommentLines(content, ["#"]);
|
|
104
93
|
const findings: Finding[] = [];
|
|
105
94
|
for (const rule of SECRET_RULES) {
|
|
106
|
-
eachMatch(rule.re,
|
|
95
|
+
eachMatch(rule.re, scan, (m) => {
|
|
107
96
|
findings.push(
|
|
108
97
|
findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }),
|
|
109
98
|
);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source detector for classical asymmetric cryptography in smart-contract /
|
|
3
|
+
* blockchain source languages: Solidity (`.sol`), Move (`.move`), and Cairo
|
|
4
|
+
* (`.cairo`). On-chain signature verification is pure classical elliptic-curve
|
|
5
|
+
* cryptography — secp256k1 ECDSA on the EVM, Ed25519 / secp256k1 on Sui & Aptos
|
|
6
|
+
* (Move), and STARK-curve ECDSA on Starknet (Cairo). A cryptographically-relevant
|
|
7
|
+
* quantum computer (CRQC) could forge any of these on-chain authorizations.
|
|
8
|
+
*
|
|
9
|
+
* HNDL: unlike a KEM or key-agreement finding, an on-chain signature check has no
|
|
10
|
+
* confidentiality to harvest — there is nothing to decrypt later — so every rule
|
|
11
|
+
* here is `hndl: false`. The exposure is nonetheless SEVERE: on-chain keys often
|
|
12
|
+
* ARE the custody of the asset (an account's authorization to move funds), so a
|
|
13
|
+
* forged signature is a direct loss, not a downgraded confidentiality guarantee.
|
|
14
|
+
* That is why the findings are `severity: high` despite `hndl: false`.
|
|
15
|
+
*
|
|
16
|
+
* There is no `secp256k1` value in {@link AlgorithmFamily}; secp256k1 usage is
|
|
17
|
+
* mapped to `ECDSA` (the scheme), and the specific curve is named in the message.
|
|
18
|
+
* The STARK-curve check is likewise `ECDSA` (ECDSA over the STARK-friendly curve).
|
|
19
|
+
*
|
|
20
|
+
* Patterns (verified against OpenZeppelin, Solady, Sui/Aptos Move, Starknet docs):
|
|
21
|
+
* - Solidity / EVM (secp256k1 ECDSA → `sol-ecrecover`):
|
|
22
|
+
* `ecrecover(` (the secp256k1 precompile), OpenZeppelin/Solady
|
|
23
|
+
* `ECDSA.recover(` / `ECDSA.tryRecover(`,
|
|
24
|
+
* `SignatureChecker.isValidSignatureNow(`, and imports of the
|
|
25
|
+
* `.../cryptography/ECDSA.sol` / `SignatureChecker.sol` (OZ) or Solady
|
|
26
|
+
* `utils/ECDSA.sol` libraries. Consolidated under one rule; each distinct
|
|
27
|
+
* token/import is its own finding (evidence), but they cannot double-fire on
|
|
28
|
+
* the same span.
|
|
29
|
+
* - Move / Sui & Aptos:
|
|
30
|
+
* `ed25519::signature_verify_strict(` / `ed25519_verify(` → EdDSA
|
|
31
|
+
* (`sol-ed25519`); `ecdsa_k1::secp256k1_verify(` /
|
|
32
|
+
* `ecdsa_k1::secp256k1_ecrecover(` → ECDSA secp256k1
|
|
33
|
+
* (`sol-secp256k1-verify`).
|
|
34
|
+
* - Cairo / Starknet:
|
|
35
|
+
* `check_ecdsa_signature(` (also the `ecdsa::check_ecdsa_signature(`
|
|
36
|
+
* qualified form) → ECDSA over the STARK curve (`cairo-ecdsa`).
|
|
37
|
+
*
|
|
38
|
+
* Comment suppression is done in-detector (Solidity/Move/Cairo all use C-style
|
|
39
|
+
* `//` line comments and `/* … *\/` block comments), so a commented-out
|
|
40
|
+
* verification call cannot fire. Offsets are preserved by the maskers, so finding
|
|
41
|
+
* line/column stay exact.
|
|
42
|
+
*/
|
|
43
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
44
|
+
import {
|
|
45
|
+
eachMatch,
|
|
46
|
+
findingFromRule,
|
|
47
|
+
hasExtension,
|
|
48
|
+
maskBlockComments,
|
|
49
|
+
maskCommentLines,
|
|
50
|
+
SMART_CONTRACT_EXTENSIONS,
|
|
51
|
+
} from "../detect-utils.js";
|
|
52
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
53
|
+
|
|
54
|
+
// Shared remediation: no PQC on-chain signature scheme is standardized on these
|
|
55
|
+
// chains yet, so the actionable step today is to inventory the exposure and track
|
|
56
|
+
// each chain's roadmap for an alternative (quantum-safe) signature scheme.
|
|
57
|
+
const REMEDIATION_ONCHAIN =
|
|
58
|
+
"No PQC on-chain signature scheme is standardized on these chains yet; inventory this on-chain " +
|
|
59
|
+
"signature verification now and track chain roadmaps (e.g. account abstraction enabling " +
|
|
60
|
+
"alternative signature schemes) for a quantum-safe migration path.";
|
|
61
|
+
|
|
62
|
+
// --- Solidity / EVM: secp256k1 ECDSA. ---
|
|
63
|
+
// The secp256k1 recovery precompile. `\b` before `ecrecover` keeps it off the
|
|
64
|
+
// Move `secp256k1_ecrecover` token (preceded by `_`, a word char → no boundary).
|
|
65
|
+
const RE_SOL_ECRECOVER = /\becrecover\s*\(/g;
|
|
66
|
+
// OpenZeppelin / Solady library calls: `ECDSA.recover(` / `ECDSA.tryRecover(`
|
|
67
|
+
// and `SignatureChecker.isValidSignatureNow(`.
|
|
68
|
+
const RE_SOL_ECDSA_LIB =
|
|
69
|
+
/\bECDSA\.(?:recover|tryRecover)\s*\(|\bSignatureChecker\.isValidSignatureNow\s*\(/g;
|
|
70
|
+
// Import of the classical-signature libraries: OZ `.../cryptography/ECDSA.sol` /
|
|
71
|
+
// `.../cryptography/SignatureChecker.sol`, and Solady `.../utils/ECDSA.sol`.
|
|
72
|
+
const RE_SOL_ECDSA_IMPORT = /(?:cryptography|utils)\/(?:ECDSA|SignatureChecker)\.sol\b/g;
|
|
73
|
+
|
|
74
|
+
// --- Move / Sui & Aptos. ---
|
|
75
|
+
// Ed25519 verification: Aptos `ed25519::signature_verify_strict(`, Sui
|
|
76
|
+
// `ed25519::ed25519_verify(` (matched via the bare `ed25519_verify(` token).
|
|
77
|
+
const RE_MOVE_ED25519 = /\bed25519::signature_verify_strict\s*\(|\bed25519_verify\s*\(/g;
|
|
78
|
+
// secp256k1 ECDSA verification / recovery via the Sui `ecdsa_k1` module.
|
|
79
|
+
const RE_MOVE_SECP256K1 = /\becdsa_k1::secp256k1_(?:verify|ecrecover)\s*\(/g;
|
|
80
|
+
|
|
81
|
+
// --- Cairo / Starknet: STARK-curve ECDSA. ---
|
|
82
|
+
// `check_ecdsa_signature(` — also matches the qualified `ecdsa::check_ecdsa_signature(`
|
|
83
|
+
// (the preceding `:` is a non-word char, so the `\b` boundary still holds).
|
|
84
|
+
const RE_CAIRO_ECDSA = /\bcheck_ecdsa_signature\s*\(/g;
|
|
85
|
+
|
|
86
|
+
const RULE_SOL_ECRECOVER: RuleMeta = {
|
|
87
|
+
id: "sol-ecrecover",
|
|
88
|
+
title: "EVM on-chain ECDSA signature verification (secp256k1)",
|
|
89
|
+
description:
|
|
90
|
+
"Solidity ecrecover / OpenZeppelin-Solady ECDSA.recover / SignatureChecker (secp256k1)",
|
|
91
|
+
category: "signature",
|
|
92
|
+
severity: "high",
|
|
93
|
+
confidence: "high",
|
|
94
|
+
algorithm: "ECDSA",
|
|
95
|
+
hndl: false,
|
|
96
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
97
|
+
message:
|
|
98
|
+
"On-chain ECDSA over secp256k1 (Solidity/EVM: ecrecover, OpenZeppelin/Solady ECDSA / SignatureChecker) is forgeable by a quantum attacker; on-chain keys often ARE asset custody.",
|
|
99
|
+
remediation: REMEDIATION_ONCHAIN,
|
|
100
|
+
};
|
|
101
|
+
const RULE_MOVE_ED25519: RuleMeta = {
|
|
102
|
+
id: "sol-ed25519",
|
|
103
|
+
title: "Move on-chain Ed25519 signature verification",
|
|
104
|
+
description: "Move (Sui/Aptos) ed25519::signature_verify_strict / ed25519_verify",
|
|
105
|
+
category: "signature",
|
|
106
|
+
severity: "high",
|
|
107
|
+
confidence: "high",
|
|
108
|
+
algorithm: "EdDSA",
|
|
109
|
+
hndl: false,
|
|
110
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
111
|
+
message:
|
|
112
|
+
"On-chain Ed25519 signature verification (Move / Sui / Aptos) is classical and forgeable by a quantum attacker; on-chain keys often ARE asset custody.",
|
|
113
|
+
remediation: REMEDIATION_ONCHAIN,
|
|
114
|
+
};
|
|
115
|
+
const RULE_MOVE_SECP256K1: RuleMeta = {
|
|
116
|
+
id: "sol-secp256k1-verify",
|
|
117
|
+
title: "Move on-chain ECDSA signature verification (secp256k1)",
|
|
118
|
+
description: "Move (Sui) ecdsa_k1::secp256k1_verify / secp256k1_ecrecover",
|
|
119
|
+
category: "signature",
|
|
120
|
+
severity: "high",
|
|
121
|
+
confidence: "high",
|
|
122
|
+
algorithm: "ECDSA",
|
|
123
|
+
hndl: false,
|
|
124
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
125
|
+
message:
|
|
126
|
+
"On-chain ECDSA over secp256k1 (Move / Sui ecdsa_k1) is forgeable by a quantum attacker; on-chain keys often ARE asset custody.",
|
|
127
|
+
remediation: REMEDIATION_ONCHAIN,
|
|
128
|
+
};
|
|
129
|
+
const RULE_CAIRO_ECDSA: RuleMeta = {
|
|
130
|
+
id: "cairo-ecdsa",
|
|
131
|
+
title: "Cairo/Starknet on-chain ECDSA signature verification (STARK curve)",
|
|
132
|
+
description: "Cairo check_ecdsa_signature (STARK-curve ECDSA)",
|
|
133
|
+
category: "signature",
|
|
134
|
+
severity: "high",
|
|
135
|
+
confidence: "high",
|
|
136
|
+
algorithm: "ECDSA",
|
|
137
|
+
hndl: false,
|
|
138
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
139
|
+
message:
|
|
140
|
+
"On-chain ECDSA over the STARK curve (Cairo/Starknet check_ecdsa_signature) is forgeable by a quantum attacker; on-chain keys often ARE asset custody.",
|
|
141
|
+
remediation: REMEDIATION_ONCHAIN,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Detects classical (quantum-vulnerable) on-chain signature verification in
|
|
146
|
+
* smart-contract source: Solidity/EVM secp256k1 ECDSA, Move Ed25519 & secp256k1,
|
|
147
|
+
* and Cairo/Starknet STARK-curve ECDSA.
|
|
148
|
+
*/
|
|
149
|
+
export const solidityDetector: Detector = {
|
|
150
|
+
id: "solidity-crypto",
|
|
151
|
+
description:
|
|
152
|
+
"Classical on-chain signature verification in smart-contract source (Solidity, Move, Cairo)",
|
|
153
|
+
scope: "source",
|
|
154
|
+
language: "solidity",
|
|
155
|
+
rules: [RULE_SOL_ECRECOVER, RULE_MOVE_ED25519, RULE_MOVE_SECP256K1, RULE_CAIRO_ECDSA],
|
|
156
|
+
appliesTo: (f) => hasExtension(f, SMART_CONTRACT_EXTENSIONS),
|
|
157
|
+
detect({ file, content }): Finding[] {
|
|
158
|
+
// Mask C-style comments (line `//` + block `/* */`) so commented-out
|
|
159
|
+
// verification calls can't fire; the maskers preserve byte offsets.
|
|
160
|
+
const scan = maskCommentLines(maskBlockComments(content), ["//"]);
|
|
161
|
+
const findings: Finding[] = [];
|
|
162
|
+
const add = (re: RegExp, rule: RuleMeta) =>
|
|
163
|
+
eachMatch(re, scan, (m) =>
|
|
164
|
+
findings.push(
|
|
165
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
add(RE_SOL_ECRECOVER, RULE_SOL_ECRECOVER);
|
|
169
|
+
add(RE_SOL_ECDSA_LIB, RULE_SOL_ECRECOVER);
|
|
170
|
+
add(RE_SOL_ECDSA_IMPORT, RULE_SOL_ECRECOVER);
|
|
171
|
+
add(RE_MOVE_ED25519, RULE_MOVE_ED25519);
|
|
172
|
+
add(RE_MOVE_SECP256K1, RULE_MOVE_SECP256K1);
|
|
173
|
+
add(RE_CAIRO_ECDSA, RULE_CAIRO_ECDSA);
|
|
174
|
+
return findings;
|
|
175
|
+
},
|
|
176
|
+
};
|