@quantakrypto/core 0.4.4 → 0.5.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/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 +53 -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/index.d.ts +14 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -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/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 +9 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +31 -1
- 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 +6 -5
- 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 +8 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +32 -1
- 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/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 +48 -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/index.ts +41 -13
- package/src/inventory.ts +15 -7
- package/src/registry.ts +51 -4
- package/src/remediate-pipeline.ts +1 -1
- package/src/remediation.ts +39 -0
- package/src/report.ts +36 -1
- package/src/scan-worker.ts +5 -3
- package/src/scan.ts +41 -6
- package/src/standards-profiles.ts +136 -0
- package/src/types.ts +9 -4
- package/src/version.ts +1 -1
- package/src/vex.ts +122 -0
- package/src/walk.ts +33 -2
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source detector for classical asymmetric cryptography in Objective-C on Apple
|
|
3
|
+
* platforms (iOS / macOS / tvOS / watchOS), via the Security framework's
|
|
4
|
+
* `SecKey*` API (and, underneath it, CommonCrypto). This is the Objective-C
|
|
5
|
+
* sibling of the Swift pack: the same Security-framework surface, reached from
|
|
6
|
+
* `.m` / `.mm` translation units instead of `.swift`. Lexical, same strategy as
|
|
7
|
+
* the other language packs — the Security-framework identifiers
|
|
8
|
+
* (`kSecAttrKeyType*`, `kSecKeyAlgorithm*`) are distinctive enough to keep the
|
|
9
|
+
* false-positive risk low.
|
|
10
|
+
*
|
|
11
|
+
* WHY a dedicated Objective-C pack (and not the C/C++ pack):
|
|
12
|
+
* - The C/C++ pack (`c.ts`) matches OpenSSL / libsodium / Mbed TLS / wolfSSL
|
|
13
|
+
* symbols and is gated to `.c/.h/.cc/.cpp/...` (which INCLUDES `.h`). Apple's
|
|
14
|
+
* Security framework is a different API surface entirely, so a separate pack
|
|
15
|
+
* is the clean home for it.
|
|
16
|
+
* - Objective-C headers are `.h` — shared with C/C++. To avoid double-scanning
|
|
17
|
+
* a header the C pack already owns, this detector is gated to `.m` and `.mm`
|
|
18
|
+
* ONLY (implementation files). Declarations of `SecKey*` usage live in the
|
|
19
|
+
* implementation, not the interface, so no real coverage is lost.
|
|
20
|
+
*
|
|
21
|
+
* Security-framework surface mapped here:
|
|
22
|
+
* - Key generation attributes (`SecKeyCreateRandomKey` / `SecKeyGeneratePair`):
|
|
23
|
+
* `kSecAttrKeyTypeRSA` → RSA keygen (kem, hndl:true)
|
|
24
|
+
* `kSecAttrKeyTypeEC` / `…ECSECPrimeRandom` → EC keygen (ECDSA, signature,
|
|
25
|
+
* hndl:false — EC keys default
|
|
26
|
+
* to signing; conservative)
|
|
27
|
+
* - Signing / verifying (`SecKeyCreateSignature` / `SecKeyVerifySignature`):
|
|
28
|
+
* `kSecKeyAlgorithmRSASignature*` → RSA signature (hndl:false)
|
|
29
|
+
* `kSecKeyAlgorithmECDSASignature*` → ECDSA signature (hndl:false)
|
|
30
|
+
* - Encryption / key agreement (`SecKeyCreateEncryptedData` / `SecKeyCopyKeyExchangeResult`):
|
|
31
|
+
* `kSecKeyAlgorithmRSAEncryption*` → RSA encryption (kem, hndl:true)
|
|
32
|
+
* `kSecKeyAlgorithmECDHKeyExchange*` → ECDH key agreement (hndl:true)
|
|
33
|
+
*
|
|
34
|
+
* Comment suppression is handled inline here (Objective-C is a C-style comment
|
|
35
|
+
* language): C-style block comments and `//` line comments are masked before the
|
|
36
|
+
* regexes run, so commented-out crypto never fires.
|
|
37
|
+
*/
|
|
38
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
39
|
+
import {
|
|
40
|
+
eachMatch,
|
|
41
|
+
findingFromRule,
|
|
42
|
+
hasExtension,
|
|
43
|
+
maskBlockComments,
|
|
44
|
+
maskCommentLines,
|
|
45
|
+
OBJC_EXTENSIONS,
|
|
46
|
+
} from "../detect-utils.js";
|
|
47
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
48
|
+
|
|
49
|
+
// Security-framework key-type attributes (used with SecKeyCreateRandomKey /
|
|
50
|
+
// SecKeyGeneratePair). RSA vs EC. The EC form covers both the modern
|
|
51
|
+
// `kSecAttrKeyTypeECSECPrimeRandom` and the legacy `kSecAttrKeyTypeEC` alias.
|
|
52
|
+
const RE_OBJC_SEC_RSA = /\bkSecAttrKeyTypeRSA\b/g;
|
|
53
|
+
const RE_OBJC_SEC_EC = /\bkSecAttrKeyType(?:ECSECPrimeRandom|EC)\b/g;
|
|
54
|
+
// SecKey algorithm constants for signing / encryption / key agreement. The `\w*`
|
|
55
|
+
// tail captures the full family suffix (…PKCS1v15SHA256, …OAEPSHA256, …Standard,
|
|
56
|
+
// …CofactorX963SHA256, …) without needing to enumerate every variant.
|
|
57
|
+
const RE_OBJC_RSA_SIGN = /\bkSecKeyAlgorithmRSASignature\w*/g;
|
|
58
|
+
const RE_OBJC_RSA_ENCRYPT = /\bkSecKeyAlgorithmRSAEncryption\w*/g;
|
|
59
|
+
const RE_OBJC_ECDSA_SIGN = /\bkSecKeyAlgorithmECDSASignature\w*/g;
|
|
60
|
+
const RE_OBJC_ECDH = /\bkSecKeyAlgorithmECDHKeyExchange\w*/g;
|
|
61
|
+
|
|
62
|
+
const RULE_OBJC_SEC_RSA: RuleMeta = {
|
|
63
|
+
id: "objc-seckey-rsa",
|
|
64
|
+
title: "Objective-C Security-framework RSA key",
|
|
65
|
+
description: "SecKeyCreateRandomKey / SecKeyGeneratePair with kSecAttrKeyTypeRSA",
|
|
66
|
+
category: "kem",
|
|
67
|
+
severity: "high",
|
|
68
|
+
confidence: "high",
|
|
69
|
+
algorithm: "RSA",
|
|
70
|
+
hndl: true,
|
|
71
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
72
|
+
message: "Security-framework RSA key (Objective-C) is classical and not quantum-safe.",
|
|
73
|
+
remediation:
|
|
74
|
+
"Migrate to PQC as Apple's CryptoKit / Security add support: ML-KEM for encryption/key-agreement, ML-DSA for signatures.",
|
|
75
|
+
};
|
|
76
|
+
const RULE_OBJC_SEC_EC: RuleMeta = {
|
|
77
|
+
id: "objc-seckey-ec",
|
|
78
|
+
title: "Objective-C Security-framework EC key",
|
|
79
|
+
description: "SecKeyCreateRandomKey / SecKeyGeneratePair with kSecAttrKeyTypeECSECPrimeRandom",
|
|
80
|
+
// An EC key at generation is AMBIGUOUS (it can feed ECDSA signing OR ECDH key
|
|
81
|
+
// agreement). The fleet convention (java-ec-keygen, python-ec-keygen, swift-sec-ec,
|
|
82
|
+
// cloud-kms-ec, …) classifies ambiguous EC keygen as key-exchange/ECDH/hndl:true —
|
|
83
|
+
// the HNDL-SAFE choice, so a possible key-agreement use is never under-reported.
|
|
84
|
+
category: "key-exchange",
|
|
85
|
+
severity: "high",
|
|
86
|
+
confidence: "high",
|
|
87
|
+
algorithm: "ECDH",
|
|
88
|
+
hndl: true,
|
|
89
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
90
|
+
message:
|
|
91
|
+
"Security-framework EC key (Objective-C); EC keys feed ECDH key agreement (harvest-now-decrypt-later exposed) as well as ECDSA signatures, and are not quantum-safe.",
|
|
92
|
+
remediation:
|
|
93
|
+
"Migrate to PQC as Apple's CryptoKit / Security add support: ML-KEM for the ECDH key-agreement path, ML-DSA for signatures.",
|
|
94
|
+
};
|
|
95
|
+
const RULE_OBJC_RSA_SIGN: RuleMeta = {
|
|
96
|
+
id: "objc-rsa-sign",
|
|
97
|
+
title: "Objective-C RSA signature",
|
|
98
|
+
description: "SecKeyCreateSignature / SecKeyVerifySignature with kSecKeyAlgorithmRSASignature*",
|
|
99
|
+
category: "signature",
|
|
100
|
+
// `high`, consistent with every sibling pack's signature severity (swift-rsa,
|
|
101
|
+
// go-rsa-sign, java-rsa-sign) — the same primitive must not flip CI exit codes
|
|
102
|
+
// based on which language wrote it.
|
|
103
|
+
severity: "high",
|
|
104
|
+
confidence: "high",
|
|
105
|
+
algorithm: "RSA",
|
|
106
|
+
hndl: false,
|
|
107
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
108
|
+
message:
|
|
109
|
+
"Classical RSA signing (Objective-C, Security framework) is forgeable by a quantum attacker.",
|
|
110
|
+
remediation: "Migrate to ML-DSA (FIPS 204) as Apple's CryptoKit / Security add PQC support.",
|
|
111
|
+
};
|
|
112
|
+
const RULE_OBJC_RSA_ENCRYPT: RuleMeta = {
|
|
113
|
+
id: "objc-rsa-encrypt",
|
|
114
|
+
title: "Objective-C RSA encryption",
|
|
115
|
+
description: "SecKeyCreateEncryptedData with kSecKeyAlgorithmRSAEncryption*",
|
|
116
|
+
category: "kem",
|
|
117
|
+
severity: "high",
|
|
118
|
+
confidence: "high",
|
|
119
|
+
algorithm: "RSA",
|
|
120
|
+
hndl: true,
|
|
121
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
122
|
+
message:
|
|
123
|
+
"Classical RSA encryption (Objective-C, Security framework) is harvest-now-decrypt-later exposed.",
|
|
124
|
+
remediation: "Migrate to ML-KEM (FIPS 203) as Apple's CryptoKit / Security add PQC support.",
|
|
125
|
+
};
|
|
126
|
+
const RULE_OBJC_ECDSA_SIGN: RuleMeta = {
|
|
127
|
+
id: "objc-ecdsa-sign",
|
|
128
|
+
title: "Objective-C ECDSA signature",
|
|
129
|
+
description: "SecKeyCreateSignature / SecKeyVerifySignature with kSecKeyAlgorithmECDSASignature*",
|
|
130
|
+
category: "signature",
|
|
131
|
+
// `high`, consistent with swift-ecdsa / go-ecdsa / java-ecdsa-sign / dart-ecdsa.
|
|
132
|
+
severity: "high",
|
|
133
|
+
confidence: "high",
|
|
134
|
+
algorithm: "ECDSA",
|
|
135
|
+
hndl: false,
|
|
136
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
137
|
+
message:
|
|
138
|
+
"Classical ECDSA signing (Objective-C, Security framework) is forgeable by a quantum attacker.",
|
|
139
|
+
remediation: "Migrate to ML-DSA (FIPS 204) as Apple's CryptoKit / Security add PQC support.",
|
|
140
|
+
};
|
|
141
|
+
const RULE_OBJC_ECDH: RuleMeta = {
|
|
142
|
+
id: "objc-ecdh",
|
|
143
|
+
title: "Objective-C ECDH key agreement",
|
|
144
|
+
description: "SecKeyCopyKeyExchangeResult with kSecKeyAlgorithmECDHKeyExchange*",
|
|
145
|
+
category: "key-exchange",
|
|
146
|
+
severity: "high",
|
|
147
|
+
confidence: "high",
|
|
148
|
+
algorithm: "ECDH",
|
|
149
|
+
hndl: true,
|
|
150
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
151
|
+
message:
|
|
152
|
+
"Elliptic-curve Diffie-Hellman (Objective-C, Security framework) is broken by Shor's algorithm (harvest-now-decrypt-later).",
|
|
153
|
+
remediation:
|
|
154
|
+
"Migrate key agreement to ML-KEM (FIPS 203) as Apple's CryptoKit / Security add PQC support.",
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Detects classical asymmetric crypto in Objective-C (Apple Security framework). */
|
|
158
|
+
export const objcDetector: Detector = {
|
|
159
|
+
id: "objc-crypto",
|
|
160
|
+
description: "Classical asymmetric crypto in Objective-C (Apple Security framework / SecKey)",
|
|
161
|
+
scope: "source",
|
|
162
|
+
language: "objc",
|
|
163
|
+
rules: [
|
|
164
|
+
RULE_OBJC_SEC_RSA,
|
|
165
|
+
RULE_OBJC_SEC_EC,
|
|
166
|
+
RULE_OBJC_RSA_SIGN,
|
|
167
|
+
RULE_OBJC_RSA_ENCRYPT,
|
|
168
|
+
RULE_OBJC_ECDSA_SIGN,
|
|
169
|
+
RULE_OBJC_ECDH,
|
|
170
|
+
],
|
|
171
|
+
appliesTo: (f) => hasExtension(f, OBJC_EXTENSIONS),
|
|
172
|
+
detect({ file, content }): Finding[] {
|
|
173
|
+
// Fast reject: none of the Security-framework surface can appear without one
|
|
174
|
+
// of these substrings, so most Objective-C files bail before any regex runs.
|
|
175
|
+
if (
|
|
176
|
+
!content.includes("SecKey") &&
|
|
177
|
+
!content.includes("kSecAttrKeyType") &&
|
|
178
|
+
!content.includes("kSecKeyAlgorithm")
|
|
179
|
+
) {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
// Mask C-style block comments then `//` line comments so commented-out crypto
|
|
183
|
+
// does not fire. Both maskers preserve byte offsets, so finding line/column
|
|
184
|
+
// stay exact.
|
|
185
|
+
const masked = maskCommentLines(maskBlockComments(content), ["//"]);
|
|
186
|
+
|
|
187
|
+
const findings: Finding[] = [];
|
|
188
|
+
// Scan the masked text, but build the finding (its snippet) from the ORIGINAL
|
|
189
|
+
// `content` so a line with a trailing comment renders live, not blanked.
|
|
190
|
+
const add = (re: RegExp, rule: RuleMeta) =>
|
|
191
|
+
eachMatch(re, masked, (m) =>
|
|
192
|
+
findings.push(
|
|
193
|
+
findingFromRule(rule, {
|
|
194
|
+
file,
|
|
195
|
+
content,
|
|
196
|
+
index: m.index,
|
|
197
|
+
matchLength: m[0].length,
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
add(RE_OBJC_SEC_RSA, RULE_OBJC_SEC_RSA);
|
|
202
|
+
add(RE_OBJC_SEC_EC, RULE_OBJC_SEC_EC);
|
|
203
|
+
add(RE_OBJC_RSA_SIGN, RULE_OBJC_RSA_SIGN);
|
|
204
|
+
add(RE_OBJC_RSA_ENCRYPT, RULE_OBJC_RSA_ENCRYPT);
|
|
205
|
+
add(RE_OBJC_ECDSA_SIGN, RULE_OBJC_ECDSA_SIGN);
|
|
206
|
+
add(RE_OBJC_ECDH, RULE_OBJC_ECDH);
|
|
207
|
+
return findings;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: BINARY OpenPGP key material and encrypted messages committed to
|
|
3
|
+
* a repository — `secring.gpg` / `pubring.gpg` / `.gpg` / `.pgp` / `.kbx`. Armored
|
|
4
|
+
* blocks (`-----BEGIN PGP …-----`, base64 text) are handled by the PEM/secrets
|
|
5
|
+
* detectors; this covers the BINARY packet form, which the scan pipeline now reads
|
|
6
|
+
* byte-preserving (latin1; see {@link ../walk.ts isKeystorePath}) so we can read
|
|
7
|
+
* packet tags and the public-key algorithm id.
|
|
8
|
+
*
|
|
9
|
+
* A committed OpenPGP SECRET key is the sharp finding: a classical private key
|
|
10
|
+
* (RSA/DSA/ElGamal/ECDSA/EdDSA/ECDH) in version control is exposed and
|
|
11
|
+
* retroactively un-fixable. A binary PGP-encrypted message (PKESK) is
|
|
12
|
+
* harvest-now-decrypt-later exposed. Secret-key matches are sensitive material, so
|
|
13
|
+
* reporters drop the (binary) snippet.
|
|
14
|
+
*
|
|
15
|
+
* Packet framing per RFC 4880/9580. The parser is pure and bounds-checked (fuzzed).
|
|
16
|
+
*/
|
|
17
|
+
import type { AlgorithmFamily, Detector, Finding, RuleMeta } from "../types.js";
|
|
18
|
+
import { findingFromRule, hasExtension } from "../detect-utils.js";
|
|
19
|
+
import { CWE_HARDCODED_KEY, CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
20
|
+
|
|
21
|
+
const OPENPGP_EXTENSIONS: readonly string[] = [".gpg", ".pgp"];
|
|
22
|
+
|
|
23
|
+
// Packet tags we care about.
|
|
24
|
+
const TAG_PKESK = 1; // public-key encrypted session key (an encrypted message)
|
|
25
|
+
const TAG_SECRET_KEY = 5;
|
|
26
|
+
const TAG_PUBLIC_KEY = 6;
|
|
27
|
+
const TAG_SECRET_SUBKEY = 7;
|
|
28
|
+
const TAG_PUBLIC_SUBKEY = 14;
|
|
29
|
+
|
|
30
|
+
interface Packet {
|
|
31
|
+
tag: number;
|
|
32
|
+
/** Offset where the packet body begins (after the length octets). */
|
|
33
|
+
bodyOffset: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Parse the FIRST OpenPGP packet header; return its tag and body offset. */
|
|
37
|
+
function firstPacket(content: string): Packet | undefined {
|
|
38
|
+
if (content.length < 2) return undefined;
|
|
39
|
+
const b0 = content.charCodeAt(0);
|
|
40
|
+
if ((b0 & 0x80) === 0) return undefined; // high bit always set on a packet tag
|
|
41
|
+
if (b0 & 0x40) {
|
|
42
|
+
// New-format packet: tag is the low 6 bits; length octet(s) follow.
|
|
43
|
+
const tag = b0 & 0x3f;
|
|
44
|
+
const l = content.charCodeAt(1);
|
|
45
|
+
let bodyOffset: number;
|
|
46
|
+
if (l < 192) bodyOffset = 2;
|
|
47
|
+
else if (l < 224) bodyOffset = 3;
|
|
48
|
+
else if (l === 255) bodyOffset = 6;
|
|
49
|
+
else bodyOffset = 2; // partial-body length
|
|
50
|
+
return { tag, bodyOffset };
|
|
51
|
+
}
|
|
52
|
+
// Old-format packet: tag in bits 5-2; length-type in the low 2 bits.
|
|
53
|
+
const tag = (b0 >> 2) & 0x0f;
|
|
54
|
+
const lt = b0 & 0x03;
|
|
55
|
+
const bodyOffset = lt === 0 ? 2 : lt === 1 ? 3 : lt === 2 ? 5 : 1;
|
|
56
|
+
return { tag, bodyOffset };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Map an OpenPGP public-key algorithm id to a family + HNDL (confidentiality) flag. */
|
|
60
|
+
function algo(id: number): { family: AlgorithmFamily; hndl: boolean } | undefined {
|
|
61
|
+
switch (id) {
|
|
62
|
+
case 1:
|
|
63
|
+
case 2:
|
|
64
|
+
case 3:
|
|
65
|
+
return { family: "RSA", hndl: true }; // RSA (encrypt-capable)
|
|
66
|
+
case 16:
|
|
67
|
+
case 20:
|
|
68
|
+
return { family: "unknown", hndl: true }; // ElGamal (encrypt)
|
|
69
|
+
case 17:
|
|
70
|
+
return { family: "DSA", hndl: false }; // DSA (sign only)
|
|
71
|
+
case 18:
|
|
72
|
+
return { family: "ECDH", hndl: true }; // ECDH (encrypt)
|
|
73
|
+
case 19:
|
|
74
|
+
return { family: "ECDSA", hndl: false }; // ECDSA (sign only)
|
|
75
|
+
case 22:
|
|
76
|
+
return { family: "EdDSA", hndl: false }; // EdDSA (sign only)
|
|
77
|
+
// RFC 9580 (crypto-refresh) v6 algorithm ids.
|
|
78
|
+
case 25:
|
|
79
|
+
return { family: "X25519", hndl: true }; // X25519 (encrypt / key agreement)
|
|
80
|
+
case 26:
|
|
81
|
+
return { family: "X448", hndl: true }; // X448 (encrypt / key agreement)
|
|
82
|
+
case 27:
|
|
83
|
+
return { family: "EdDSA", hndl: false }; // Ed25519 (sign only)
|
|
84
|
+
case 28:
|
|
85
|
+
return { family: "EdDSA", hndl: false }; // Ed448 (sign only)
|
|
86
|
+
default:
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Read the public-key algorithm id from a key packet body, or undefined. */
|
|
92
|
+
function keyPacketAlgo(content: string, bodyOffset: number): number | undefined {
|
|
93
|
+
if (bodyOffset >= content.length) return undefined;
|
|
94
|
+
const version = content.charCodeAt(bodyOffset);
|
|
95
|
+
// v2/v3: version(1) created(4) validity(2) algo; v4/v5/v6: version(1) created(4) algo.
|
|
96
|
+
const algoOffset = version === 2 || version === 3 ? bodyOffset + 7 : bodyOffset + 5;
|
|
97
|
+
if (algoOffset >= content.length) return undefined;
|
|
98
|
+
return content.charCodeAt(algoOffset);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Read the wrapping algorithm from a PKESK (encrypted-message) packet body. */
|
|
102
|
+
function pkeskAlgo(content: string, bodyOffset: number): number | undefined {
|
|
103
|
+
if (bodyOffset >= content.length) return undefined;
|
|
104
|
+
const version = content.charCodeAt(bodyOffset);
|
|
105
|
+
// v3: version(1) keyid(8) algo. (v6 differs; v3 dominates on-disk.)
|
|
106
|
+
if (version !== 3) return undefined;
|
|
107
|
+
const algoOffset = bodyOffset + 9;
|
|
108
|
+
if (algoOffset >= content.length) return undefined;
|
|
109
|
+
return content.charCodeAt(algoOffset);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function rule(id: string, title: string, message: string, opts: Partial<RuleMeta>): RuleMeta {
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
title,
|
|
116
|
+
description: message,
|
|
117
|
+
category: "certificate",
|
|
118
|
+
severity: "high",
|
|
119
|
+
confidence: "high",
|
|
120
|
+
hndl: false,
|
|
121
|
+
cwe: CWE_HARDCODED_KEY,
|
|
122
|
+
message,
|
|
123
|
+
remediation:
|
|
124
|
+
"Remove key material from version control and rotate it; store keys in a secret manager / HSM. Plan re-issuance with PQC keys (ML-DSA-65 / ML-KEM-768) as OpenPGP tooling supports them.",
|
|
125
|
+
...opts,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const RULE_SECRET = rule(
|
|
130
|
+
"openpgp-secret-key",
|
|
131
|
+
"OpenPGP secret key (binary)",
|
|
132
|
+
"A binary OpenPGP SECRET key is committed to version control — a classical private key that is exposed and retroactively un-fixable.",
|
|
133
|
+
{ sensitive: true },
|
|
134
|
+
);
|
|
135
|
+
const RULE_PUBLIC = rule(
|
|
136
|
+
"openpgp-public-key",
|
|
137
|
+
"OpenPGP public key (binary)",
|
|
138
|
+
"A binary OpenPGP public key uses classical asymmetric crypto (RSA/DSA/EC); plan a PQC migration path.",
|
|
139
|
+
{ severity: "medium" },
|
|
140
|
+
);
|
|
141
|
+
const RULE_PKESK = rule(
|
|
142
|
+
"openpgp-encrypted-message",
|
|
143
|
+
"OpenPGP-encrypted message (binary)",
|
|
144
|
+
"A binary OpenPGP-encrypted message wraps its session key with classical asymmetric crypto (RSA/ElGamal/ECDH); harvest-now-decrypt-later exposed.",
|
|
145
|
+
{ category: "kem", hndl: true, cwe: CWE_BROKEN_CRYPTO },
|
|
146
|
+
);
|
|
147
|
+
const RULE_KEYBOX = rule(
|
|
148
|
+
"openpgp-keybox",
|
|
149
|
+
"GnuPG keybox (.kbx)",
|
|
150
|
+
"A GnuPG keybox (.kbx) is a database of classical OpenPGP/X.509 keys and certificates.",
|
|
151
|
+
{ severity: "medium", cwe: CWE_BROKEN_CRYPTO },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
/** Detects binary OpenPGP key material / messages and GnuPG keyboxes. */
|
|
155
|
+
export const openpgpDetector: Detector = {
|
|
156
|
+
id: "openpgp-material",
|
|
157
|
+
description: "Binary OpenPGP key material / encrypted messages and GnuPG keyboxes",
|
|
158
|
+
scope: "config",
|
|
159
|
+
language: "any",
|
|
160
|
+
rules: [RULE_SECRET, RULE_PUBLIC, RULE_PKESK, RULE_KEYBOX],
|
|
161
|
+
appliesTo: (f) => hasExtension(f, OPENPGP_EXTENSIONS) || f.toLowerCase().endsWith(".kbx"),
|
|
162
|
+
detect({ file, content }): Finding[] {
|
|
163
|
+
const at = { file, content, index: 0, matchLength: Math.min(4, content.length) };
|
|
164
|
+
|
|
165
|
+
// GnuPG keybox: a key/cert database, identified by the "KBXf" magic in its
|
|
166
|
+
// first blob header (so a garbage/empty .kbx does not fire).
|
|
167
|
+
if (file.toLowerCase().endsWith(".kbx")) {
|
|
168
|
+
return content.includes("KBXf") ? [findingFromRule(RULE_KEYBOX, at)] : [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const pkt = firstPacket(content);
|
|
172
|
+
if (!pkt) return [];
|
|
173
|
+
const { tag, bodyOffset } = pkt;
|
|
174
|
+
|
|
175
|
+
if (tag === TAG_SECRET_KEY || tag === TAG_SECRET_SUBKEY) {
|
|
176
|
+
const id = keyPacketAlgo(content, bodyOffset);
|
|
177
|
+
const a = id !== undefined ? algo(id) : undefined;
|
|
178
|
+
return [
|
|
179
|
+
findingFromRule(RULE_SECRET, at, a ? { algorithm: a.family, hndl: a.hndl } : undefined),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
if (tag === TAG_PUBLIC_KEY || tag === TAG_PUBLIC_SUBKEY) {
|
|
183
|
+
const id = keyPacketAlgo(content, bodyOffset);
|
|
184
|
+
const a = id !== undefined ? algo(id) : undefined;
|
|
185
|
+
return [findingFromRule(RULE_PUBLIC, at, a ? { algorithm: a.family } : undefined)];
|
|
186
|
+
}
|
|
187
|
+
if (tag === TAG_PKESK) {
|
|
188
|
+
const id = pkeskAlgo(content, bodyOffset);
|
|
189
|
+
const a = id !== undefined ? algo(id) : undefined;
|
|
190
|
+
return [findingFromRule(RULE_PKESK, at, a ? { algorithm: a.family } : undefined)];
|
|
191
|
+
}
|
|
192
|
+
return [];
|
|
193
|
+
},
|
|
194
|
+
};
|
package/src/detectors/pem.ts
CHANGED
|
@@ -211,6 +211,31 @@ const PEM_RULES: PemRule[] = [
|
|
|
211
211
|
},
|
|
212
212
|
];
|
|
213
213
|
|
|
214
|
+
/**
|
|
215
|
+
* True when what follows the begin marker looks like a REAL PEM block rather than a
|
|
216
|
+
* `-----BEGIN …-----` header STRING LITERAL (as appears in PEM parsers, tests, and
|
|
217
|
+
* i18n messages). `from` points just past the begin marker. Three accepting signals:
|
|
218
|
+
*
|
|
219
|
+
* (a) a full LINE of base64 (≥24 chars, optional `=` padding) — real multi-line PEM
|
|
220
|
+
* bodies are line-wrapped base64; requiring the whole line to be base64 stops a
|
|
221
|
+
* long camelCase identifier on a code line from validating a bare header constant.
|
|
222
|
+
* (b) an ESCAPED-newline base64 run (`\n<base64>`, a LITERAL backslash-n) — a key
|
|
223
|
+
* embedded in a single-line JSON/.env string (GCP service accounts, `PRIVATE_KEY=
|
|
224
|
+
* "…\n…"`), whose body can run past the window before its `-----END`. The literal
|
|
225
|
+
* `\n` escape distinguishes it from a source constant, which has a REAL newline.
|
|
226
|
+
* (c) a matching `-----END …-----` marker with NO double-quote between it and the
|
|
227
|
+
* begin marker. This admits short/placeholder bodies (`x`, `abc`, `MIIB` in tests)
|
|
228
|
+
* while rejecting a parser's PAIRED header/footer constants, whose intervening
|
|
229
|
+
* `";` … `= "` always contains a `"` — a char a real PEM body never holds.
|
|
230
|
+
*/
|
|
231
|
+
function hasBase64Body(content: string, from: number): boolean {
|
|
232
|
+
const window = content.slice(from, from + 800);
|
|
233
|
+
if (/^[ \t]*[A-Za-z0-9+/]{24,}={0,2}[ \t]*$/m.test(window)) return true;
|
|
234
|
+
if (/\\n[A-Za-z0-9+/]{24,}/.test(window)) return true;
|
|
235
|
+
const end = window.search(/-----END [A-Z0-9 ]+-----/);
|
|
236
|
+
return end >= 0 && !window.slice(0, end).includes('"');
|
|
237
|
+
}
|
|
238
|
+
|
|
214
239
|
/** Detects PEM key/certificate material in arbitrary files. */
|
|
215
240
|
export const pemDetector: Detector = {
|
|
216
241
|
id: "pem-material",
|
|
@@ -227,6 +252,11 @@ export const pemDetector: Detector = {
|
|
|
227
252
|
const findings: Finding[] = [];
|
|
228
253
|
for (const rule of PEM_RULES) {
|
|
229
254
|
eachMatch(rule.re, content, (m) => {
|
|
255
|
+
// Require an actual base64 body after the marker, so a bare
|
|
256
|
+
// `-----BEGIN RSA PRIVATE KEY-----` string literal in a PEM parser / test /
|
|
257
|
+
// i18n message does NOT get reported as an embedded key. A real PEM block has
|
|
258
|
+
// a run of base64 within the next few hundred chars.
|
|
259
|
+
if (!hasBase64Body(content, m.index + m[0].length)) return;
|
|
230
260
|
findings.push(
|
|
231
261
|
findingFromRule(rule.meta, {
|
|
232
262
|
file,
|
package/src/detectors/php.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - **ext/openssl** — `openssl_pkey_new()` (classified by its
|
|
7
7
|
* `OPENSSL_KEYTYPE_*` config, defaulting to RSA), `openssl_public_encrypt` /
|
|
8
|
-
* `openssl_private_decrypt`
|
|
8
|
+
* `openssl_private_decrypt` / `openssl_seal` / `openssl_open` (RSA), and
|
|
9
|
+
* `openssl_sign` / `openssl_verify`.
|
|
9
10
|
* - **phpseclib3** — `RSA::createKey()` / `EC::createKey()` / `DSA::createKey()`.
|
|
10
11
|
* - **libsodium** — `sodium_crypto_box`/`kx` keypairs (X25519) and
|
|
11
12
|
* `sodium_crypto_sign` keypairs (Ed25519).
|
|
@@ -23,14 +24,18 @@ import { PHP_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../det
|
|
|
23
24
|
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
24
25
|
|
|
25
26
|
const RE_PHP_PKEY_NEW = /\bopenssl_pkey_new\s*\(/g;
|
|
26
|
-
|
|
27
|
+
// public_encrypt/private_decrypt (direct RSA) plus seal/open (RSA-envelope: seal
|
|
28
|
+
// encrypts a random symmetric key under each recipient's RSA public key, open
|
|
29
|
+
// decrypts it with the private key) — all RSA key transport, harvest-now exposed.
|
|
30
|
+
const RE_PHP_RSA_CRYPT = /\bopenssl_(?:public_encrypt|private_decrypt|seal|open)\s*\(/g;
|
|
27
31
|
const RE_PHP_SIGN = /\bopenssl_(?:sign|verify)\s*\(/g;
|
|
28
32
|
// phpseclib3 factory methods. The class names are generic, so require `::createKey`.
|
|
29
33
|
const RE_PHP_SECLIB = /\b(RSA|EC|DSA|DH)::createKey\s*\(/g;
|
|
30
34
|
// libsodium X25519 (box / kx / scalarmult) and Ed25519 (sign) key pairs.
|
|
31
35
|
const RE_PHP_SODIUM_X25519 =
|
|
32
36
|
/\bsodium_crypto_(?:box|kx)_(?:seed_)?keypair\s*\(|\bsodium_crypto_scalarmult(?:_base)?\s*\(/g;
|
|
33
|
-
|
|
37
|
+
// Also the explicit-algorithm variant sodium_crypto_sign_ed25519_keypair().
|
|
38
|
+
const RE_PHP_SODIUM_ED25519 = /\bsodium_crypto_sign_(?:ed25519_|seed_)?keypair\s*\(/g;
|
|
34
39
|
|
|
35
40
|
/** Key-type classification for `openssl_pkey_new`, keyed by its config constant. */
|
|
36
41
|
interface KeyInfo {
|
|
@@ -58,7 +63,7 @@ const RULE_PHP_KEYGEN: RuleMeta = {
|
|
|
58
63
|
const RULE_PHP_RSA_CRYPT: RuleMeta = {
|
|
59
64
|
id: "php-openssl-rsa-crypt",
|
|
60
65
|
title: "PHP openssl RSA public-key encryption",
|
|
61
|
-
description: "openssl_public_encrypt / openssl_private_decrypt",
|
|
66
|
+
description: "openssl_public_encrypt / openssl_private_decrypt / openssl_seal / openssl_open",
|
|
62
67
|
category: "kem",
|
|
63
68
|
severity: "high",
|
|
64
69
|
confidence: "high",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical asymmetric keys held in / generated by a PKCS#11 HSM
|
|
3
|
+
* or token. HSMs hold an organization's longest-lived roots (CA keys, code-signing
|
|
4
|
+
* keys), so they are exactly the keys a PQC migration must find first — yet they
|
|
5
|
+
* live behind a PKCS#11 layer the source packs never see.
|
|
6
|
+
*
|
|
7
|
+
* Covered, distinctive lexical surfaces:
|
|
8
|
+
* - `pkcs11-tool --keypairgen --key-type rsa:2048 | EC:secp256r1` — the standard
|
|
9
|
+
* OpenSC keygen invocation recorded in ops scripts / runbooks.
|
|
10
|
+
* - `CKM_RSA_PKCS(_KEY_PAIR_GEN|_OAEP|_PSS)` / `CKM_EC(DSA|DH)` / `CKM_DSA` — the
|
|
11
|
+
* PKCS#11 mechanism constants (SoftHSM, SunPKCS11, pkcs11js, python-pkcs11), which
|
|
12
|
+
* name the exact algorithm.
|
|
13
|
+
*/
|
|
14
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
15
|
+
import { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
16
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
17
|
+
|
|
18
|
+
// `pkcs11-tool --keypairgen --key-type rsa:2048` / `EC:secp256r1` (OpenSC).
|
|
19
|
+
const RE_PKCS11_TOOL_RSA = /--key-type\s+rsa:\d+/gi;
|
|
20
|
+
const RE_PKCS11_TOOL_EC = /--key-type\s+EC:[A-Za-z0-9]+/gi;
|
|
21
|
+
// PKCS#11 mechanism constants (CKM_*), which name the algorithm exactly.
|
|
22
|
+
const RE_CKM_RSA = /\bCKM_RSA_(?:PKCS(?:_KEY_PAIR_GEN|_OAEP|_PSS)?|X_509|9796)\b/g;
|
|
23
|
+
const RE_CKM_EC = /\bCKM_EC(?:DSA(?:_SHA\d+)?|DH1?_DERIVE|_KEY_PAIR_GEN)\b/g;
|
|
24
|
+
const RE_CKM_DSA = /\bCKM_DSA(?:_SHA\d+|_KEY_PAIR_GEN)?\b/g;
|
|
25
|
+
const RE_CKM_DH = /\bCKM_DH_PKCS_(?:KEY_PAIR_GEN|DERIVE)\b/g;
|
|
26
|
+
|
|
27
|
+
const RULE_PKCS11_RSA: RuleMeta = {
|
|
28
|
+
id: "pkcs11-rsa",
|
|
29
|
+
title: "PKCS#11 (HSM) RSA key",
|
|
30
|
+
description: "PKCS#11 RSA key generation / mechanism (pkcs11-tool, CKM_RSA_*)",
|
|
31
|
+
category: "kem",
|
|
32
|
+
severity: "high",
|
|
33
|
+
confidence: "high",
|
|
34
|
+
algorithm: "RSA",
|
|
35
|
+
hndl: true,
|
|
36
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
37
|
+
message:
|
|
38
|
+
"A classical RSA key in a PKCS#11 HSM / token — likely a long-lived root (CA / code-signing), and not quantum-safe.",
|
|
39
|
+
remediation:
|
|
40
|
+
"Inventory HSM-held RSA keys first (longest-lived); plan re-keying to PQC (ML-KEM-768 / ML-DSA-65) as HSM firmware adds support.",
|
|
41
|
+
};
|
|
42
|
+
const RULE_PKCS11_EC: RuleMeta = {
|
|
43
|
+
id: "pkcs11-ec",
|
|
44
|
+
title: "PKCS#11 (HSM) EC key",
|
|
45
|
+
description: "PKCS#11 EC key generation / mechanism (CKM_EC*, --key-type EC)",
|
|
46
|
+
category: "key-exchange",
|
|
47
|
+
severity: "high",
|
|
48
|
+
confidence: "high",
|
|
49
|
+
algorithm: "ECDH",
|
|
50
|
+
hndl: true,
|
|
51
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
52
|
+
message:
|
|
53
|
+
"A classical EC key in a PKCS#11 HSM / token; EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
|
|
54
|
+
remediation:
|
|
55
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204). Prioritise HSM-held keys.",
|
|
56
|
+
};
|
|
57
|
+
const RULE_PKCS11_DSA: RuleMeta = {
|
|
58
|
+
id: "pkcs11-dsa",
|
|
59
|
+
title: "PKCS#11 (HSM) DSA key",
|
|
60
|
+
description: "PKCS#11 DSA key generation / mechanism (CKM_DSA*)",
|
|
61
|
+
category: "signature",
|
|
62
|
+
severity: "medium",
|
|
63
|
+
confidence: "high",
|
|
64
|
+
algorithm: "DSA",
|
|
65
|
+
hndl: false,
|
|
66
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
67
|
+
message: "A classical DSA key in a PKCS#11 HSM / token — deprecated and quantum-forgeable.",
|
|
68
|
+
remediation: "Rotate off DSA; migrate to ML-DSA-65 (FIPS 204).",
|
|
69
|
+
};
|
|
70
|
+
const RULE_PKCS11_DH: RuleMeta = {
|
|
71
|
+
id: "pkcs11-dh",
|
|
72
|
+
title: "PKCS#11 (HSM) Diffie-Hellman",
|
|
73
|
+
description: "PKCS#11 finite-field DH key generation / derive (CKM_DH_PKCS_*)",
|
|
74
|
+
category: "key-exchange",
|
|
75
|
+
severity: "medium",
|
|
76
|
+
confidence: "high",
|
|
77
|
+
algorithm: "DH",
|
|
78
|
+
hndl: true,
|
|
79
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
80
|
+
message:
|
|
81
|
+
"Finite-field Diffie-Hellman in a PKCS#11 HSM / token; classical key agreement, harvest-now-decrypt-later exposed.",
|
|
82
|
+
remediation: "Migrate key agreement to hybrid X25519MLKEM768 (ML-KEM-768).",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Detects classical asymmetric keys behind a PKCS#11 HSM / token. */
|
|
86
|
+
export const pkcs11Detector: Detector = {
|
|
87
|
+
id: "pkcs11-crypto",
|
|
88
|
+
description: "Classical asymmetric keys in a PKCS#11 HSM / token",
|
|
89
|
+
scope: "config",
|
|
90
|
+
language: "any",
|
|
91
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
92
|
+
rules: [RULE_PKCS11_RSA, RULE_PKCS11_EC, RULE_PKCS11_DSA, RULE_PKCS11_DH],
|
|
93
|
+
detect({ file, content }): Finding[] {
|
|
94
|
+
// Fast reject: only proceed on a distinctive PKCS#11 marker.
|
|
95
|
+
if (!content.includes("CKM_") && !content.includes("--key-type")) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const findings: Finding[] = [];
|
|
99
|
+
const add = (re: RegExp, rule: RuleMeta): void =>
|
|
100
|
+
eachMatch(re, content, (m) =>
|
|
101
|
+
findings.push(
|
|
102
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
add(RE_PKCS11_TOOL_RSA, RULE_PKCS11_RSA);
|
|
106
|
+
add(RE_PKCS11_TOOL_EC, RULE_PKCS11_EC);
|
|
107
|
+
add(RE_CKM_RSA, RULE_PKCS11_RSA);
|
|
108
|
+
add(RE_CKM_EC, RULE_PKCS11_EC);
|
|
109
|
+
add(RE_CKM_DSA, RULE_PKCS11_DSA);
|
|
110
|
+
add(RE_CKM_DH, RULE_PKCS11_DH);
|
|
111
|
+
return findings;
|
|
112
|
+
},
|
|
113
|
+
};
|