@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
package/src/detectors/source.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
DOC_EXTENSIONS,
|
|
33
33
|
JWT_HOST_EXTENSIONS,
|
|
34
34
|
eachMatch,
|
|
35
|
+
enclosingObject,
|
|
35
36
|
findingFromRule,
|
|
36
37
|
hasExtension,
|
|
37
38
|
nearSortedCall,
|
|
@@ -46,6 +47,15 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
|
|
|
46
47
|
// (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
|
|
47
48
|
const RE_GENERATE_KEYPAIR =
|
|
48
49
|
/generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
|
|
50
|
+
// Variable-typed key generation: `generateKeyPair(kind, …)` where the algorithm
|
|
51
|
+
// argument is an IDENTIFIER (a variable), not a quoted literal, so the concrete
|
|
52
|
+
// family is unknown at scan time but a classical key is still being generated.
|
|
53
|
+
// The lookbehind anchors the call to a word boundary so it does not fire inside
|
|
54
|
+
// a longer identifier (e.g. `myGenerateKeyPair(`); the first argument must be a
|
|
55
|
+
// bare identifier immediately followed by `,` or `)` so a quoted-literal call
|
|
56
|
+
// (handled by RE_GENERATE_KEYPAIR) never matches here.
|
|
57
|
+
const RE_GENERATE_KEYPAIR_VAR =
|
|
58
|
+
/(?<![\w$])generateKeyPair(?:Sync)?\s*\(\s*([A-Za-z_$][\w$]*)\s*[,)]/g;
|
|
49
59
|
|
|
50
60
|
/** Per-key-type classification for `generateKeyPair(Sync)('<type>', …)`. Hoisted
|
|
51
61
|
* to module scope so the direct matcher AND the import-alias pass (below) share
|
|
@@ -176,6 +186,20 @@ function escapeRe(s: string): string {
|
|
|
176
186
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
177
187
|
}
|
|
178
188
|
const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
|
|
189
|
+
// Computed-member (bracket) access to a Node `crypto` method:
|
|
190
|
+
// `crypto['createSign'](…)`, `c["createECDH"](…)`. Bracket access defeats the
|
|
191
|
+
// dotted-call regexes above (which require `name(`), so match a quoted method
|
|
192
|
+
// name inside a `[ '…' ]( ` computed call and route it to the right rule. The
|
|
193
|
+
// method names here take no key-type argument, so no argument parsing is needed;
|
|
194
|
+
// `generateKeyPair(Sync)` bracket access is handled separately (it carries the
|
|
195
|
+
// key-type literal). None of these overlap the dotted regexes (those require the
|
|
196
|
+
// name immediately before `(`, whereas here it is followed by a closing quote).
|
|
197
|
+
const RE_BRACKET_CRYPTO_METHOD =
|
|
198
|
+
/\[\s*['"`](createSign|createVerify|createECDH|createDiffieHellman|createDiffieHellmanGroup|publicEncrypt|privateDecrypt)['"`]\s*\]\s*\(/g;
|
|
199
|
+
// `crypto['generateKeyPairSync']('rsa', …)`: bracket access carrying the quoted
|
|
200
|
+
// key-type literal, classified exactly like the dotted `generateKeyPair` call.
|
|
201
|
+
const RE_BRACKET_KEYGEN =
|
|
202
|
+
/\[\s*['"`]generateKeyPair(?:Sync)?['"`]\s*\]\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
|
|
179
203
|
// One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
|
|
180
204
|
// consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
|
|
181
205
|
// or `createSign(` (handled by the dedicated createSign/createVerify rule) —
|
|
@@ -225,11 +249,15 @@ const RE_TLS_REJECT = /rejectUnauthorized\s*:\s*false/g;
|
|
|
225
249
|
// Hardened cipher regex: bounded spans (no unbounded `[^'"`]*` straddling the
|
|
226
250
|
// alternation), single-quote-style anchoring removed in favour of {0,256} bounds
|
|
227
251
|
// so worst-case backtracking is linear in the bound, not the file (P0-6).
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
252
|
+
// Skip OpenSSL EXCLUSION syntax — `!MD5` / `:-RC4` / `!ECDHE-RSA-RC4-SHA` DISABLE
|
|
253
|
+
// those ciphers, so a hardened list must not be flagged as weak (audit: crypto #7). The
|
|
254
|
+
// single variable-length lookbehind walks back over the suite-name chars to the
|
|
255
|
+
// ELEMENT boundary (`:` / whitespace / comma / quote / start) and rejects the match
|
|
256
|
+
// when that element begins with `!` or `-` — covering a bare `!RC4`, a space-separated
|
|
257
|
+
// `HIGH !RC4`, and a full-suite `!ECDHE-RSA-RC4-SHA`. The intra-name hyphen in a
|
|
258
|
+
// genuinely-enabled `ECDHE-RSA-RC4-SHA` (no leading `!`/`-`) still matches.
|
|
231
259
|
const RE_TLS_WEAK_CIPHER =
|
|
232
|
-
/ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![!-])(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
|
|
260
|
+
/ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![:'"`,\s]\s*[!-][\w-]{0,64})(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
|
|
233
261
|
|
|
234
262
|
/* -------------------------------------------------------------------------- */
|
|
235
263
|
/* Node.js `crypto` module */
|
|
@@ -370,6 +398,28 @@ const nodeCryptoDetector: Detector = {
|
|
|
370
398
|
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
|
|
371
399
|
});
|
|
372
400
|
|
|
401
|
+
// generateKeyPair(Sync)(<variable>, …): key type passed as a variable, so the
|
|
402
|
+
// family is unknown; emit the generic keygen rule (unknown / key-exchange /
|
|
403
|
+
// HNDL-conservative) rather than missing the classical key generation.
|
|
404
|
+
eachMatch(RE_GENERATE_KEYPAIR_VAR, content, (m) => {
|
|
405
|
+
findings.push(
|
|
406
|
+
findingFromRule(
|
|
407
|
+
RULE_NODE_KEYGEN,
|
|
408
|
+
{ file, content, index: m.index, matchLength: m[0].length },
|
|
409
|
+
{
|
|
410
|
+
title: "Classical key generation (variable key type)",
|
|
411
|
+
message:
|
|
412
|
+
"Generates a classical asymmetric key pair (key type passed as a variable), which is not quantum-safe.",
|
|
413
|
+
},
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Bracket (computed-member) key generation: crypto['generateKeyPairSync']('rsa').
|
|
419
|
+
eachMatch(RE_BRACKET_KEYGEN, content, (m) => {
|
|
420
|
+
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
|
|
421
|
+
});
|
|
422
|
+
|
|
373
423
|
// Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
|
|
374
424
|
// the CommonJS destructure-rename) so an aliased call still detects. Only the
|
|
375
425
|
// keygen / ECDH / DH constructors are resolved — their classification is
|
|
@@ -427,6 +477,23 @@ const nodeCryptoDetector: Detector = {
|
|
|
427
477
|
);
|
|
428
478
|
});
|
|
429
479
|
|
|
480
|
+
// Bracket (computed-member) access to argument-free crypto methods:
|
|
481
|
+
// crypto['createSign'](…), c["createECDH"](…). Route each to its rule.
|
|
482
|
+
eachMatch(RE_BRACKET_CRYPTO_METHOD, content, (m) => {
|
|
483
|
+
const loc = { file, content, index: m.index, matchLength: m[0].length };
|
|
484
|
+
const method = m[1];
|
|
485
|
+
if (method === "createSign" || method === "createVerify") {
|
|
486
|
+
findings.push(findingFromRule(RULE_NODE_SIGN, loc));
|
|
487
|
+
} else if (method === "createECDH") {
|
|
488
|
+
findings.push(findingFromRule(RULE_NODE_ECDH, loc));
|
|
489
|
+
} else if (method === "publicEncrypt" || method === "privateDecrypt") {
|
|
490
|
+
findings.push(findingFromRule(RULE_NODE_RSA_ENCRYPT, loc));
|
|
491
|
+
} else {
|
|
492
|
+
// createDiffieHellman / createDiffieHellmanGroup: finite-field DH.
|
|
493
|
+
findings.push(findingFromRule(RULE_NODE_DH, loc));
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
|
|
430
497
|
// One-shot crypto.sign(algorithm, data, key) / crypto.verify(...) (Node ≥ 12).
|
|
431
498
|
eachMatch(RE_ONESHOT_SIGN_VERIFY, content, (m) => {
|
|
432
499
|
findings.push(
|
|
@@ -561,7 +628,9 @@ const webCryptoDetector: Detector = {
|
|
|
561
628
|
algorithm = name === "X448" ? "X448" : "X25519";
|
|
562
629
|
category = "key-exchange";
|
|
563
630
|
hndl = true;
|
|
564
|
-
|
|
631
|
+
// `medium`, aligned with X25519/X448 key agreement in node/rust/go — HNDL-exposed
|
|
632
|
+
// key agreement scores the same regardless of which surface uses it.
|
|
633
|
+
severity = "medium";
|
|
565
634
|
} else if (name === "ED25519" || name === "ED448") {
|
|
566
635
|
algorithm = "EdDSA";
|
|
567
636
|
category = "signature";
|
|
@@ -818,8 +887,21 @@ const jwtDetector: Detector = {
|
|
|
818
887
|
detect({ file, content }): Finding[] {
|
|
819
888
|
const findings: Finding[] = [];
|
|
820
889
|
|
|
890
|
+
// Deferral guards against double-counting:
|
|
891
|
+
// - webCryptoDetector owns a quoted `RSA-OAEP`/`ECDH-ES` sitting next to a
|
|
892
|
+
// `subtle.*(` call (it matches the `RSA-OAEP`/`ECDH` name). Collect the subtle
|
|
893
|
+
// call offsets so the JOSE key-management loops can skip those. (In non-JS host
|
|
894
|
+
// files there are no subtle calls, so nothing is skipped.)
|
|
895
|
+
// - jwkDetector owns any alg/`kty` that lives inside a JWK object; skip a token
|
|
896
|
+
// whose enclosing `{…}` carries a `"kty"` so a JWK's own `alg` is not counted by
|
|
897
|
+
// both detectors. Mirrors the same guard jose.ts applies for other languages.
|
|
898
|
+
const subtleCalls: number[] = [];
|
|
899
|
+
eachMatch(RE_SUBTLE_CALL, content, (m) => subtleCalls.push(m.index));
|
|
900
|
+
const inJwk = (index: number): boolean => enclosingObject(content, index).includes('"kty"');
|
|
901
|
+
|
|
821
902
|
// Classical JWS signature alg tokens. Anchored to quotes to avoid words.
|
|
822
903
|
eachMatch(RE_JWT_ALG, content, (m) => {
|
|
904
|
+
if (inJwk(m.index)) return; // jwk-{rsa,ec,eddsa} owns a JWK's own alg
|
|
823
905
|
const alg = m[1];
|
|
824
906
|
let algorithm: Finding["algorithm"];
|
|
825
907
|
if (alg.startsWith("RS") || alg.startsWith("PS")) algorithm = "RSA";
|
|
@@ -840,6 +922,8 @@ const jwtDetector: Detector = {
|
|
|
840
922
|
|
|
841
923
|
// JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
|
|
842
924
|
eachMatch(RE_JOSE_ECDH, content, (m) => {
|
|
925
|
+
if (inJwk(m.index)) return; // jwk-ec owns a JWK's own alg
|
|
926
|
+
if (nearSortedCall(subtleCalls, m.index, 400)) return; // webcrypto owns it
|
|
843
927
|
findings.push(
|
|
844
928
|
findingFromRule(
|
|
845
929
|
RULE_JOSE_ECDH,
|
|
@@ -854,6 +938,10 @@ const jwtDetector: Detector = {
|
|
|
854
938
|
|
|
855
939
|
// JOSE RSA key transport (RSA-OAEP / RSA1_5) — classical RSA encryption, HNDL.
|
|
856
940
|
eachMatch(RE_JOSE_KEM, content, (m) => {
|
|
941
|
+
if (inJwk(m.index)) return; // jwk-rsa owns a JWK's own alg
|
|
942
|
+
// webCryptoDetector's regex matches `RSA-OAEP` but NOT `RSA1_5`, so only defer the
|
|
943
|
+
// OAEP form near a subtle call — deferring RSA1_5 would drop it entirely.
|
|
944
|
+
if (m[1].startsWith("RSA-OAEP") && nearSortedCall(subtleCalls, m.index, 400)) return;
|
|
857
945
|
findings.push(
|
|
858
946
|
findingFromRule(
|
|
859
947
|
RULE_JOSE_RSA_OAEP,
|
|
@@ -981,6 +1069,21 @@ const RE_CERT_SIG_ALG =
|
|
|
981
1069
|
const RE_SSH_KEX =
|
|
982
1070
|
/\b(diffie-hellman-group(?:1|14|15|16|17|18)(?:-sha1|-sha256|-sha512)?|diffie-hellman-group-exchange-sha(?:1|256)|ecdh-sha2-nistp(?:256|384|521)|curve25519-sha256)\b/g;
|
|
983
1071
|
|
|
1072
|
+
/**
|
|
1073
|
+
* True when the match at `index` sits inside a DOCUMENTATION field value — a
|
|
1074
|
+
* `description` / `help` / `summary` / `doc` / `comment` / `note` assignment
|
|
1075
|
+
* (`key = "…"` or `key: "…"`), as used by Terraform/Packer variable descriptions,
|
|
1076
|
+
* JSON-schema docs, etc. Classical KEX/cipher algorithm names LISTED in such a field
|
|
1077
|
+
* are prose ("acceptable values include ecdh-sha2-nistp256, …"), not an active
|
|
1078
|
+
* setting, so a transport-token rule must not fire on them. Scans the current line's
|
|
1079
|
+
* prefix only (cheap; the common single-line description case).
|
|
1080
|
+
*/
|
|
1081
|
+
function inDocFieldValue(content: string, index: number): boolean {
|
|
1082
|
+
const lineStart = content.lastIndexOf("\n", index - 1) + 1;
|
|
1083
|
+
const prefix = content.slice(lineStart, index);
|
|
1084
|
+
return /\b(?:description|help|summary|doc|comment|note)\b\s*[:=]/i.test(prefix);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
984
1087
|
// A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
|
|
985
1088
|
// when it is EITHER (a) followed by base64 key material — an actual
|
|
986
1089
|
// authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
|
|
@@ -1128,6 +1231,7 @@ const sshCertDetector: Detector = {
|
|
|
1128
1231
|
|
|
1129
1232
|
// SSH key-exchange algorithm identifiers (finite-field DH / ECDH / X25519).
|
|
1130
1233
|
eachMatch(RE_SSH_KEX, content, (m) => {
|
|
1234
|
+
if (inDocFieldValue(content, m.index)) return; // names listed in a doc string
|
|
1131
1235
|
const tok = m[1];
|
|
1132
1236
|
const algorithm: Finding["algorithm"] = tok.startsWith("diffie-hellman")
|
|
1133
1237
|
? "DH"
|
|
@@ -1196,6 +1300,7 @@ const tlsClassicalKexDetector: Detector = {
|
|
|
1196
1300
|
detect({ file, content }): Finding[] {
|
|
1197
1301
|
const findings: Finding[] = [];
|
|
1198
1302
|
eachMatch(RE_TLS_CLASSICAL_KEX, content, (m) => {
|
|
1303
|
+
if (inDocFieldValue(content, m.index)) return; // names listed in a doc string
|
|
1199
1304
|
const tok = m[0];
|
|
1200
1305
|
const algorithm: Finding["algorithm"] = tok.includes("ECDH") ? "ECDH" : "DH";
|
|
1201
1306
|
findings.push(
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical key types configured for SPIFFE X.509 SVIDs in
|
|
3
|
+
* SPIRE server/agent configuration (HCL, and the `.conf`/YAML/JSON variants
|
|
4
|
+
* SPIRE deployments ship).
|
|
5
|
+
*
|
|
6
|
+
* SPIRE is the reference SPIFFE implementation: it issues each workload a
|
|
7
|
+
* short-lived X.509-SVID certificate, signed by the SPIRE server's CA (itself
|
|
8
|
+
* often chained to an UpstreamAuthority). The SVID leaf key and the CA signing
|
|
9
|
+
* key are configured by explicit key-type strings:
|
|
10
|
+
* - server `ca_key_type = "rsa-2048" | "rsa-4096" | "ec-p256" | "ec-p384"` —
|
|
11
|
+
* the CA that signs every workload SVID in the trust domain (the identity
|
|
12
|
+
* ROOT: forge this and you forge the whole trust domain).
|
|
13
|
+
* - agent/server `svid_key_type = "rsa-2048" | "ec-p256"` — the per-workload
|
|
14
|
+
* leaf key type.
|
|
15
|
+
* - per-plugin `key_type = "rsa-2048" | "ec-p256"` on UpstreamAuthority /
|
|
16
|
+
* KeyManager plugins.
|
|
17
|
+
*
|
|
18
|
+
* These are SIGNING / identity keys, not confidentiality keys: an SVID X.509
|
|
19
|
+
* signature is verified, never used to wrap harvestable ciphertext. So there is
|
|
20
|
+
* nothing to "harvest now, decrypt later" — `hndl: false` — but every SVID
|
|
21
|
+
* signature (and the CA signature over it) becomes FORGEABLE the moment a
|
|
22
|
+
* cryptographically-relevant quantum computer (CRQC) exists, letting an attacker
|
|
23
|
+
* mint certificates for any SPIFFE ID in the trust domain. Findings are
|
|
24
|
+
* therefore `category: "certificate"` (X.509 / PKI material), `severity:
|
|
25
|
+
* "medium"`, `confidence: "high"`.
|
|
26
|
+
*
|
|
27
|
+
* Algorithm mapping (SPIRE key-type value → {@link AlgorithmFamily}):
|
|
28
|
+
* - `rsa-2048` / `rsa-4096` / any `rsa-*` → RSA
|
|
29
|
+
* - `ec-p256` / `ec-p384` / `ecdsa` → ECDSA
|
|
30
|
+
*
|
|
31
|
+
* Match shape: the key-type attribute name (`ca_key_type`, `svid_key_type`, or
|
|
32
|
+
* the bare per-plugin `key_type`) followed by `=`/`:` and a quoted value, in
|
|
33
|
+
* both HCL (`=`) and YAML/JSON (`:`) forms. The `\b(?:ca_|svid_)?key_type\b`
|
|
34
|
+
* anchor covers all three names in one pattern — the leading `\b` won't fire
|
|
35
|
+
* inside `ca_key_type`/`svid_key_type` for the bare `key_type` alternative
|
|
36
|
+
* because `_` is a word character, so those longer names are matched whole.
|
|
37
|
+
*
|
|
38
|
+
* Fast reject: `detect()` bails unless the file carries a SPIFFE/SPIRE-specific
|
|
39
|
+
* marker (`spiffe`, `spire`, the distinctive `ca_key_type`, or `svid`). A bare
|
|
40
|
+
* `key_type = "rsa-2048"` is deliberately NOT a sufficient marker — `key_type`
|
|
41
|
+
* is a generic attribute (Vault PKI roles, cloud IaC, …), so without a
|
|
42
|
+
* spiffe/spire/svid/ca_key_type anchor an unrelated config must not fire. This
|
|
43
|
+
* keeps the generic per-plugin `key_type` rule precise while still catching a
|
|
44
|
+
* real SPIRE config, which always names its trust domain / server / SVIDs.
|
|
45
|
+
*/
|
|
46
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
47
|
+
import {
|
|
48
|
+
DOC_EXTENSIONS,
|
|
49
|
+
eachMatch,
|
|
50
|
+
findingFromRule,
|
|
51
|
+
hasExtension,
|
|
52
|
+
maskCommentLines,
|
|
53
|
+
} from "../detect-utils.js";
|
|
54
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
55
|
+
|
|
56
|
+
// SPIRE config is HCL/conf, but deployments also template it as YAML (Helm
|
|
57
|
+
// values / ConfigMaps) or JSON. Prose (`.md`, …) is excluded in `appliesTo`.
|
|
58
|
+
const SPIRE_EXTENSIONS: readonly string[] = [".conf", ".hcl", ".yaml", ".yml", ".json"];
|
|
59
|
+
|
|
60
|
+
// The three SPIRE key-type attribute names in one anchor: `ca_key_type`,
|
|
61
|
+
// `svid_key_type`, and the bare per-plugin `key_type`.
|
|
62
|
+
const KEY_ATTR = "(?:ca_|svid_)?key_type";
|
|
63
|
+
// HCL `=`, unquoted YAML `:`, and JSON `"key_type": "..."` assignment forms. The
|
|
64
|
+
// attribute-name and value quotes are BOTH optional (`"?`): HCL uses `key_type =
|
|
65
|
+
// "rsa-2048"`, Helm/YAML uses the unquoted `key_type: rsa-2048`, and JSON quotes
|
|
66
|
+
// the key (`"key_type": "rsa-2048"`) — a required-quote regex missed the latter
|
|
67
|
+
// two entirely. Case-insensitive, though SPIRE writes these lowercase.
|
|
68
|
+
const RE_SPIRE_RSA = new RegExp(`\\b${KEY_ATTR}"?\\s*[:=]\\s*"?rsa-\\d+`, "gi");
|
|
69
|
+
const RE_SPIRE_ECDSA = new RegExp(`\\b${KEY_ATTR}"?\\s*[:=]\\s*"?(?:ec-p\\d+|ecdsa)\\b`, "gi");
|
|
70
|
+
|
|
71
|
+
const RULE_SPIRE_RSA: RuleMeta = {
|
|
72
|
+
id: "spire-rsa-key",
|
|
73
|
+
title: "SPIRE/SPIFFE RSA SVID key type",
|
|
74
|
+
description: 'SPIRE ca_key_type/svid_key_type/key_type = "rsa-*" (classical X.509-SVID key)',
|
|
75
|
+
category: "certificate",
|
|
76
|
+
severity: "medium",
|
|
77
|
+
confidence: "high",
|
|
78
|
+
algorithm: "RSA",
|
|
79
|
+
hndl: false,
|
|
80
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
81
|
+
message:
|
|
82
|
+
"SPIRE issues SPIFFE X.509-SVIDs with a classical RSA key; the SVID (and CA) signatures become forgeable once a CRQC exists, letting an attacker mint identities for the trust domain.",
|
|
83
|
+
remediation:
|
|
84
|
+
"SPIFFE/SPIRE has no PQC SVID key type yet — track the SPIFFE roadmap for PQC signature support. Prioritise the SPIRE server ca_key_type: the classical CA is the identity root, so its forgeability compromises every workload SVID.",
|
|
85
|
+
};
|
|
86
|
+
const RULE_SPIRE_ECDSA: RuleMeta = {
|
|
87
|
+
id: "spire-ec-key",
|
|
88
|
+
title: "SPIRE/SPIFFE ECDSA SVID key type",
|
|
89
|
+
description:
|
|
90
|
+
'SPIRE ca_key_type/svid_key_type/key_type = "ec-p256|ec-p384|ecdsa" (classical X.509-SVID key)',
|
|
91
|
+
category: "certificate",
|
|
92
|
+
severity: "medium",
|
|
93
|
+
confidence: "high",
|
|
94
|
+
algorithm: "ECDSA",
|
|
95
|
+
hndl: false,
|
|
96
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
97
|
+
message:
|
|
98
|
+
"SPIRE issues SPIFFE X.509-SVIDs with a classical ECDSA (P-256/P-384) key; the SVID (and CA) signatures become forgeable once a CRQC exists, letting an attacker mint identities for the trust domain.",
|
|
99
|
+
remediation:
|
|
100
|
+
"SPIFFE/SPIRE has no PQC SVID key type yet — track the SPIFFE roadmap for PQC signature support. Prioritise the SPIRE server ca_key_type: the classical CA is the identity root, so its forgeability compromises every workload SVID.",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* True when `content` carries a SPIFFE/SPIRE-specific marker. A bare generic
|
|
105
|
+
* `key_type` is intentionally NOT enough — only `spiffe`/`spire` context, the
|
|
106
|
+
* distinctive `ca_key_type`, or an `svid` token qualifies — so an unrelated
|
|
107
|
+
* config that merely sets `key_type = "rsa-2048"` cannot fire.
|
|
108
|
+
*/
|
|
109
|
+
function hasSpireMarker(content: string): boolean {
|
|
110
|
+
// Word-anchored so `inspired` / `aspire` / a `svid` substring inside a longer
|
|
111
|
+
// token don't qualify; `ca_key_type` is distinctive enough to match unanchored.
|
|
112
|
+
return /\b(?:spiffe|spire|svid)\b/i.test(content) || content.includes("ca_key_type");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Detects classical key types configured for SPIFFE X.509 SVIDs in SPIRE config. */
|
|
116
|
+
export const spireDetector: Detector = {
|
|
117
|
+
id: "spire-crypto",
|
|
118
|
+
description: "Classical key types for SPIFFE X.509 SVIDs in SPIRE server/agent config",
|
|
119
|
+
scope: "config",
|
|
120
|
+
language: "any",
|
|
121
|
+
rules: [RULE_SPIRE_RSA, RULE_SPIRE_ECDSA],
|
|
122
|
+
// Gate to SPIRE's config extensions, and never run on prose (a README that
|
|
123
|
+
// describes `ca_key_type = "rsa-2048"` is documentation, not live config).
|
|
124
|
+
appliesTo: (f) => hasExtension(f, SPIRE_EXTENSIONS) && !hasExtension(f, DOC_EXTENSIONS),
|
|
125
|
+
detect({ file, content }): Finding[] {
|
|
126
|
+
if (!hasSpireMarker(content)) return [];
|
|
127
|
+
|
|
128
|
+
// Mask whole comment lines (HCL/YAML `#`, HCL/JSON5 `//`) so a commented-out
|
|
129
|
+
// key-type directive can't fire. Offsets are preserved, so finding
|
|
130
|
+
// line/column/snippet for the remaining live config stay exact.
|
|
131
|
+
const scan = maskCommentLines(content, ["#", "//"]);
|
|
132
|
+
const findings: Finding[] = [];
|
|
133
|
+
const add = (re: RegExp, rule: RuleMeta) =>
|
|
134
|
+
eachMatch(re, scan, (m) =>
|
|
135
|
+
findings.push(
|
|
136
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
add(RE_SPIRE_RSA, RULE_SPIRE_RSA);
|
|
140
|
+
add(RE_SPIRE_ECDSA, RULE_SPIRE_ECDSA);
|
|
141
|
+
return findings;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical SSH **certificate-authority** cryptography — the
|
|
3
|
+
* OpenSSH certificate mechanism, in which a CA key signs host/user certificates
|
|
4
|
+
* (`*-cert-v01@openssh.com`) that servers and clients then trust in place of
|
|
5
|
+
* bare public keys.
|
|
6
|
+
*
|
|
7
|
+
* This is DIFFERENT from the `ssh-kex-classical` rule in `source.ts`, and from
|
|
8
|
+
* the `ssh-public-key` rule there — deliberately non-overlapping:
|
|
9
|
+
* - `ssh-kex-classical` is about the SSH *key-exchange* (finite-field DH, ECDH,
|
|
10
|
+
* curve25519): session-key *agreement*, `category: "key-exchange"`,
|
|
11
|
+
* `hndl: true` — a passively-recorded handshake is decryptable once a CRQC
|
|
12
|
+
* exists (harvest-now-decrypt-later confidentiality loss).
|
|
13
|
+
* - `ssh-public-key` flags bare `ssh-rsa AAAA…` / `ecdsa-sha2-* …` key lines.
|
|
14
|
+
* - THIS detector flags the SSH **certificate / CA signing** surface: the
|
|
15
|
+
* `*-cert-v01@openssh.com` cert key types, the `@cert-authority` known_hosts
|
|
16
|
+
* marker, the `TrustedUserCAKeys` / `HostCertificate` sshd directives, and
|
|
17
|
+
* `ssh-keygen -s` (the command that signs a certificate with a CA key).
|
|
18
|
+
*
|
|
19
|
+
* WHY it matters for PQC: an SSH certificate is a *signature* made by a CA key
|
|
20
|
+
* over a principal's public key + validity constraints. There is no
|
|
21
|
+
* confidentiality to harvest (so `hndl: false`), but every such signature
|
|
22
|
+
* becomes FORGEABLE the moment a cryptographically-relevant quantum computer
|
|
23
|
+
* (CRQC) can recover the classical CA private key from its public key. Forging a
|
|
24
|
+
* CA signature lets an attacker mint host or user certificates that every relying
|
|
25
|
+
* party accepts — a full trust-root compromise. SSH CA keys are also unusually
|
|
26
|
+
* long-lived (they are the anchor an entire fleet trusts and are rotated
|
|
27
|
+
* rarely), so the exposure window is large. Hence `category: "signature"`,
|
|
28
|
+
* `severity: "medium"`, `confidence: "high"`.
|
|
29
|
+
*
|
|
30
|
+
* Two match shapes:
|
|
31
|
+
* 1. The cert key type, which NAMES the signing algorithm → an accurate
|
|
32
|
+
* {@link AlgorithmFamily}:
|
|
33
|
+
* - `ssh-rsa-cert-v01@openssh.com`, `rsa-sha2-256/512-cert-v01@openssh.com` → RSA
|
|
34
|
+
* - `ecdsa-sha2-nistp256/384/521-cert-v01@openssh.com` → ECDSA
|
|
35
|
+
* - `ssh-ed25519-cert-v01@openssh.com` → EdDSA
|
|
36
|
+
* 2. The CA *deployment* directives — `TrustedUserCAKeys` / `HostCertificate`
|
|
37
|
+
* (sshd_config) and `ssh-keygen -s` (the CA signing command). These are the
|
|
38
|
+
* MOST COMMON way an SSH CA is configured, but they don't name the algorithm
|
|
39
|
+
* (that lives in the referenced key file), so they emit the `ssh-ca-config`
|
|
40
|
+
* rule with `algorithm: "unknown"` and `confidence: "medium"`.
|
|
41
|
+
*
|
|
42
|
+
* Precision:
|
|
43
|
+
* - Fast reject: `detect()` bails unless the file carries an SSH-CA marker
|
|
44
|
+
* (`cert-v01@openssh.com`, `@cert-authority`, `TrustedUserCAKeys`,
|
|
45
|
+
* `HostCertificate`, or `ssh-keygen -s`) — so the algorithm tokens can't be
|
|
46
|
+
* reached on unrelated config.
|
|
47
|
+
* - The cert-type tokens are the full `*-cert-v01@openssh.com` strings. The
|
|
48
|
+
* trailing `-cert-v01@openssh.com` suffix distinguishes a CA certificate type
|
|
49
|
+
* from the plain `ssh-rsa` / `ssh-ed25519` public-key tokens handled by
|
|
50
|
+
* `ssh-public-key`, so a non-cert `ssh-ed25519 AAAA…` line NEVER fires a cert
|
|
51
|
+
* rule here.
|
|
52
|
+
* - Comment lines (`#`) are masked with {@link maskCommentLines} (offsets
|
|
53
|
+
* preserved) so a commented-out cert line can't fire. Known_hosts
|
|
54
|
+
* `@cert-authority` lines start with `@`, not `#`, so masking leaves them
|
|
55
|
+
* live — exactly what we want.
|
|
56
|
+
* - Doc/prose files are excluded ({@link DOC_EXTENSIONS}), and so are program
|
|
57
|
+
* SOURCE files ({@link ANALYZABLE_SOURCE_EXTENSIONS}) — a vendored SSH library
|
|
58
|
+
* or constants table that spells a cert type as a string literal is not live
|
|
59
|
+
* CA config. SSH CA config lives in extensionless / `.conf` / `.pub` files.
|
|
60
|
+
*
|
|
61
|
+
* The matched line frequently carries the CA public key's base64 blob (a
|
|
62
|
+
* `@cert-authority` known_hosts entry), so findings are marked `sensitive` and
|
|
63
|
+
* reporters drop the snippet, consistent with `ssh-public-key`.
|
|
64
|
+
*/
|
|
65
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
66
|
+
import {
|
|
67
|
+
eachMatch,
|
|
68
|
+
findingFromRule,
|
|
69
|
+
hasExtension,
|
|
70
|
+
maskCommentLines,
|
|
71
|
+
DOC_EXTENSIONS,
|
|
72
|
+
ANALYZABLE_SOURCE_EXTENSIONS,
|
|
73
|
+
} from "../detect-utils.js";
|
|
74
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
75
|
+
|
|
76
|
+
// --- OpenSSH certificate key types (`*-cert-v01@openssh.com`), by signing family. ---
|
|
77
|
+
// The `\b` after `.com` is a real boundary (`m` is a word char); the leading `\b`
|
|
78
|
+
// keeps `ssh-rsa-cert-v01@…` from matching inside a longer token. RSA covers the
|
|
79
|
+
// legacy `ssh-rsa` cert type plus the SHA-2 RSA cert types (`rsa-sha2-256/512`)
|
|
80
|
+
// OpenSSH emits for RSA CA/host certificates.
|
|
81
|
+
const RE_CERT_RSA = /\b(?:ssh-rsa|rsa-sha2-(?:256|512))-cert-v01@openssh\.com\b/g;
|
|
82
|
+
const RE_CERT_ECDSA = /\becdsa-sha2-nistp(?:256|384|521)-cert-v01@openssh\.com\b/g;
|
|
83
|
+
const RE_CERT_EDDSA = /\bssh-ed25519-cert-v01@openssh\.com\b/g;
|
|
84
|
+
// The canonical CA *deployment* directives — the most common way an SSH CA is
|
|
85
|
+
// configured (sshd_config `TrustedUserCAKeys` / `HostCertificate`, and the CA
|
|
86
|
+
// signing command `ssh-keygen -s`). These name the CA usage but not the algorithm
|
|
87
|
+
// (that lives in the referenced key file), so the finding is `algorithm: "unknown"`.
|
|
88
|
+
const RE_CA_DIRECTIVE = /\bTrustedUserCAKeys\b|\bHostCertificate\b|\bssh-keygen\s+-s\b/g;
|
|
89
|
+
|
|
90
|
+
const REMEDIATION =
|
|
91
|
+
"No post-quantum SSH certificate format is standardized yet — track OpenSSH release notes and IETF work on PQC signatures for SSH. SSH CA keys are long-lived trust roots, so plan for their rotation to a PQC signing algorithm (e.g. ML-DSA) as soon as a cert format lands, and keep validity periods short in the interim.";
|
|
92
|
+
|
|
93
|
+
const RULE_CA_RSA: RuleMeta = {
|
|
94
|
+
id: "ssh-ca-rsa-cert",
|
|
95
|
+
title: "SSH certificate authority — RSA signing",
|
|
96
|
+
description: "OpenSSH RSA certificate key type (ssh-rsa/rsa-sha2-*-cert-v01@openssh.com)",
|
|
97
|
+
category: "signature",
|
|
98
|
+
severity: "medium",
|
|
99
|
+
confidence: "high",
|
|
100
|
+
algorithm: "RSA",
|
|
101
|
+
hndl: false,
|
|
102
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
103
|
+
sensitive: true,
|
|
104
|
+
message:
|
|
105
|
+
"SSH certificate uses a classical RSA key type (ssh-rsa/rsa-sha2-*-cert-v01@openssh.com); the CA signature is forgeable once a CRQC can recover the RSA CA key.",
|
|
106
|
+
remediation: REMEDIATION,
|
|
107
|
+
};
|
|
108
|
+
const RULE_CA_ECDSA: RuleMeta = {
|
|
109
|
+
id: "ssh-ca-ecdsa-cert",
|
|
110
|
+
title: "SSH certificate authority — ECDSA signing",
|
|
111
|
+
description: "OpenSSH ECDSA certificate key type (ecdsa-sha2-nistp*-cert-v01@openssh.com)",
|
|
112
|
+
category: "signature",
|
|
113
|
+
severity: "medium",
|
|
114
|
+
confidence: "high",
|
|
115
|
+
algorithm: "ECDSA",
|
|
116
|
+
hndl: false,
|
|
117
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
118
|
+
sensitive: true,
|
|
119
|
+
message:
|
|
120
|
+
"SSH certificate uses a classical ECDSA key type (ecdsa-sha2-nistp*-cert-v01@openssh.com); the CA signature is forgeable once a CRQC can recover the ECDSA CA key.",
|
|
121
|
+
remediation: REMEDIATION,
|
|
122
|
+
};
|
|
123
|
+
const RULE_CA_EDDSA: RuleMeta = {
|
|
124
|
+
id: "ssh-ca-ed25519-cert",
|
|
125
|
+
title: "SSH certificate authority — EdDSA signing",
|
|
126
|
+
description: "OpenSSH Ed25519 certificate key type (ssh-ed25519-cert-v01@openssh.com)",
|
|
127
|
+
category: "signature",
|
|
128
|
+
severity: "medium",
|
|
129
|
+
confidence: "high",
|
|
130
|
+
algorithm: "EdDSA",
|
|
131
|
+
hndl: false,
|
|
132
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
133
|
+
sensitive: true,
|
|
134
|
+
message:
|
|
135
|
+
"SSH certificate uses a classical Ed25519 key type (ssh-ed25519-cert-v01@openssh.com); modern but still classical — the CA signature is forgeable once a CRQC can recover the Ed25519 CA key.",
|
|
136
|
+
remediation: REMEDIATION,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const RULE_CA_CONFIG: RuleMeta = {
|
|
140
|
+
id: "ssh-ca-config",
|
|
141
|
+
title: "SSH certificate authority configured",
|
|
142
|
+
description:
|
|
143
|
+
"OpenSSH CA deployment directive (TrustedUserCAKeys / HostCertificate / ssh-keygen -s)",
|
|
144
|
+
category: "signature",
|
|
145
|
+
severity: "medium",
|
|
146
|
+
// The directive names CA usage, not the algorithm — hence a lower confidence and
|
|
147
|
+
// an unknown family; the CA key file itself carries the specific algorithm.
|
|
148
|
+
confidence: "medium",
|
|
149
|
+
algorithm: "unknown",
|
|
150
|
+
hndl: false,
|
|
151
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
152
|
+
message:
|
|
153
|
+
"SSH certificate authority is configured (TrustedUserCAKeys / HostCertificate / ssh-keygen -s); the CA signing key is a long-lived classical trust root whose signatures become forgeable once a CRQC exists — verify the CA key's algorithm.",
|
|
154
|
+
remediation: REMEDIATION,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
interface SshCaRule {
|
|
158
|
+
meta: RuleMeta;
|
|
159
|
+
re: RegExp;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const SSH_CA_RULES: readonly SshCaRule[] = [
|
|
163
|
+
{ meta: RULE_CA_RSA, re: RE_CERT_RSA },
|
|
164
|
+
{ meta: RULE_CA_ECDSA, re: RE_CERT_ECDSA },
|
|
165
|
+
{ meta: RULE_CA_EDDSA, re: RE_CERT_EDDSA },
|
|
166
|
+
{ meta: RULE_CA_CONFIG, re: RE_CA_DIRECTIVE },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
/** True when `content` carries some SSH-CA-specific marker (not just a bare token). */
|
|
170
|
+
function hasSshCaMarker(content: string): boolean {
|
|
171
|
+
return (
|
|
172
|
+
content.includes("cert-v01@openssh.com") ||
|
|
173
|
+
content.includes("@cert-authority") ||
|
|
174
|
+
content.includes("TrustedUserCAKeys") ||
|
|
175
|
+
content.includes("HostCertificate") ||
|
|
176
|
+
content.includes("ssh-keygen -s")
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Detects classical SSH certificate-authority signing (OpenSSH cert key types)
|
|
182
|
+
* in SSH config, sshd_config, and known_hosts files. Distinct from SSH
|
|
183
|
+
* key-exchange (`ssh-kex-classical`): this is the certificate SIGNATURE surface.
|
|
184
|
+
*/
|
|
185
|
+
export const sshCaDetector: Detector = {
|
|
186
|
+
id: "ssh-ca",
|
|
187
|
+
description: "Classical SSH certificate-authority signing (OpenSSH *-cert-v01@openssh.com)",
|
|
188
|
+
scope: "config",
|
|
189
|
+
language: "any",
|
|
190
|
+
rules: SSH_CA_RULES.map((r) => r.meta),
|
|
191
|
+
// Apply broadly (SSH CA config lives in many differently-named / extensionless
|
|
192
|
+
// files: sshd_config, known_hosts, *.pub) but never on prose/docs OR on program
|
|
193
|
+
// SOURCE files — a vendored SSH library or a constants table that spells out
|
|
194
|
+
// `ssh-ed25519-cert-v01@openssh.com` as a string literal is not a live CA config.
|
|
195
|
+
// The strict fast-reject in detect() is the remaining gate.
|
|
196
|
+
appliesTo: (f) =>
|
|
197
|
+
!hasExtension(f, DOC_EXTENSIONS) && !hasExtension(f, ANALYZABLE_SOURCE_EXTENSIONS),
|
|
198
|
+
detect({ file, content }): Finding[] {
|
|
199
|
+
if (!hasSshCaMarker(content)) return [];
|
|
200
|
+
|
|
201
|
+
// Mask `#` comment lines so a commented-out cert line can't fire; offsets are
|
|
202
|
+
// preserved. `@cert-authority` known_hosts lines start with `@`, not `#`, so
|
|
203
|
+
// they are intentionally left live.
|
|
204
|
+
const scan = maskCommentLines(content, ["#"]);
|
|
205
|
+
const findings: Finding[] = [];
|
|
206
|
+
for (const { meta, re } of SSH_CA_RULES) {
|
|
207
|
+
eachMatch(re, scan, (m) =>
|
|
208
|
+
findings.push(
|
|
209
|
+
findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return findings;
|
|
214
|
+
},
|
|
215
|
+
};
|