@cdxgen/cdxgen 13.6.0 → 13.7.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 +15 -8
- package/bin/audit.js +7 -4
- package/bin/cdxgen.js +256 -37
- package/bin/convert.js +146 -22
- package/bin/evinse.js +25 -3
- package/bin/repl.js +47 -3
- package/bin/sign.js +211 -34
- package/bin/tracebom.js +18 -6
- package/bin/validate.js +18 -3
- package/bin/verify.js +113 -5
- package/data/behavior-taxonomy.schema.json +1491 -0
- package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
- package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
- package/data/pqc-migration.json +239 -0
- package/data/remediations.json +52 -0
- package/data/rules/cbom-security.yaml +4 -3
- package/data/rules/package-integrity.yaml +28 -0
- package/lib/audit/index.js +25 -3
- package/lib/cli/bomAssembly.js +80 -1
- package/lib/cli/cliOptions.js +121 -17
- package/lib/cli/index.js +609 -898
- package/lib/cli/jsBom.js +3 -0
- package/lib/cli/jvmBom.js +365 -40
- package/lib/cli/managedBom.js +19 -3
- package/lib/cli/nativeBom.js +10 -0
- package/lib/core/env.js +26 -0
- package/lib/core/fs.js +126 -3
- package/lib/core/globPatterns.js +218 -0
- package/lib/core/processTree.js +99 -12
- package/lib/core/projectConfig.js +332 -25
- package/lib/core/toolRegistry.js +15 -1
- package/lib/ecosystems/denoutils.js +2 -2
- package/lib/ecosystems/golem.js +3 -8
- package/lib/ecosystems/gradleutils.js +1082 -6
- package/lib/ecosystems/jsEvidence.js +90 -38
- package/lib/ecosystems/kosi.js +3 -5
- package/lib/ecosystems/npmVersionResolution.js +376 -0
- package/lib/ecosystems/parsers-python.js +89 -28
- package/lib/ecosystems/parsers-rust.js +117 -15
- package/lib/ecosystems/remote/huggingface.js +22 -0
- package/lib/ecosystems/rusi.js +2 -4
- package/lib/ecosystems/scalasem.js +2 -1
- package/lib/evinser/evinser.js +196 -34
- package/lib/evinser/scalasem.js +5 -1
- package/lib/helpers/agenticToolutils.js +112 -1
- package/lib/helpers/bomSigner.js +43 -9
- package/lib/helpers/jssSigner.js +1276 -0
- package/lib/inventory/agentBlueprint.js +193 -0
- package/lib/inventory/agentToolNames.js +145 -0
- package/lib/inventory/aiCollector.js +153 -7
- package/lib/inventory/aiInventory.js +44 -8
- package/lib/inventory/aiProviders.js +324 -0
- package/lib/inventory/bomUtils.js +8 -1
- package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
- package/lib/inventory/citations.js +31 -2
- package/lib/inventory/communityAiConfigParser.js +978 -80
- package/lib/inventory/cyclonedx20.js +152 -0
- package/lib/inventory/cyclonedx20Common.js +286 -0
- package/lib/inventory/cyclonedx20Derived.js +159 -0
- package/lib/inventory/cyclonedx20Parties.js +552 -0
- package/lib/inventory/cyclonedx20Read.js +768 -0
- package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
- package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
- package/lib/inventory/cyclonedx20ReadContext.js +380 -0
- package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
- package/lib/inventory/cyclonedx20ReadSections.js +599 -0
- package/lib/inventory/cyclonedx20Render.js +766 -0
- package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
- package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
- package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
- package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
- package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
- package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
- package/lib/inventory/cyclonedx20RenderSections.js +499 -0
- package/lib/inventory/cyclonedx20Tables.js +594 -0
- package/lib/inventory/deps.js +106 -13
- package/lib/inventory/dosai.js +236 -1
- package/lib/inventory/dosaiParsers.js +3 -6
- package/lib/inventory/dotnetRuntime.js +164 -14
- package/lib/inventory/ecosystemFormulationParsers.js +979 -0
- package/lib/inventory/envcontext.js +132 -38
- package/lib/inventory/evidenceUtils.js +2 -1
- package/lib/inventory/formulationParsers.js +95 -13
- package/lib/inventory/hbom.js +45 -6
- package/lib/inventory/hbomLoader.js +166 -0
- package/lib/inventory/mcpConfigParser.js +5 -0
- package/lib/inventory/occurrenceKey.js +90 -0
- package/lib/inventory/pqcMigration.js +426 -0
- package/lib/inventory/rustFormulationParser.js +21 -10
- package/lib/inventory/source.js +65 -6
- package/lib/parsers/huggingfaceManifest.js +9 -1
- package/lib/parsers/huggingfaceUtils.js +18 -0
- package/lib/parsers/kiroPermissions.js +166 -0
- package/lib/server/server.js +36 -34
- package/lib/stages/postgen/postgen.js +41 -1
- package/lib/stages/postgen/specVersionCompat.js +76 -0
- package/lib/validator/bomValidator.js +8 -1
- package/lib/validator/complianceEngine.js +30 -3
- package/lib/validator/complianceRules.js +1416 -4
- package/lib/validator/generated/validate-2.0.mjs +57554 -13851
- package/lib/validator/index.js +48 -7
- package/lib/validator/reporters/json.js +6 -0
- package/package.json +9 -9
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +6 -2
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +16 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +6 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +2 -1
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +15 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +30 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +10 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +4 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/globPatterns.d.ts +70 -0
- package/types/lib/core/globPatterns.d.ts.map +1 -0
- package/types/lib/core/paths.d.ts +9 -0
- package/types/lib/core/paths.d.ts.map +1 -1
- package/types/lib/core/processTree.d.ts +16 -5
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts +47 -8
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +248 -8
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
- package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts +58 -16
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +61 -9
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/scalasem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +53 -19
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +20 -3
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/helpers/jssSigner.d.ts +288 -0
- package/types/lib/helpers/jssSigner.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +303 -19
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/inventory/agentBlueprint.d.ts +70 -0
- package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
- package/types/lib/inventory/agentToolNames.d.ts +51 -0
- package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/aiProviders.d.ts +85 -0
- package/types/lib/inventory/aiProviders.d.ts.map +1 -0
- package/types/lib/inventory/bomUtils.d.ts.map +1 -1
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
- package/types/lib/inventory/citations.d.ts +5 -0
- package/types/lib/inventory/citations.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/cyclonedx20.d.ts +99 -0
- package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +35 -2
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +19 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +22 -6
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts +11 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomLoader.d.ts +54 -0
- package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/occurrenceKey.d.ts +52 -0
- package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
- package/types/lib/inventory/pqcMigration.d.ts +84 -0
- package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
- package/types/lib/inventory/scalaDedupe.d.ts +43 -0
- package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +15 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/kiroPermissions.d.ts +62 -0
- package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +7 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts +5 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceEngine.d.ts +4 -4
- package/types/lib/validator/complianceEngine.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts +14 -2
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
|
@@ -0,0 +1,1276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Signature Scheme (JSS) signing and verification for CycloneDX 2.0.
|
|
3
|
+
*
|
|
4
|
+
* Implemented from Recommendation ITU-T X.590 (10/2023), "JSON signature
|
|
5
|
+
* scheme". Clause citations below refer to that text:
|
|
6
|
+
*
|
|
7
|
+
* - clause 6.2.1: the signature object members (`hash_algorithm`,
|
|
8
|
+
* `algorithm`, `public_key`, `value`, the countersignature `signature`)
|
|
9
|
+
* and, in its footnote, that one of `public_key`, `public_cert_chain`,
|
|
10
|
+
* `cert_url` or `thumbprint` is present;
|
|
11
|
+
* - clause 6.2.2 and Table 1: the algorithm vocabulary;
|
|
12
|
+
* - clause 7.1.2: existing signatures are temporarily removed;
|
|
13
|
+
* - clause 7.1.3: the new signature object (without its value) is added;
|
|
14
|
+
* - clause 7.1.4: the entire JSON object is canonicalized with JCS
|
|
15
|
+
* [IETF RFC 8785];
|
|
16
|
+
* - clause 7.1.5: the JCS version is hashed with `hash_algorithm`, and the
|
|
17
|
+
* hash is presented in (lowercase) hex;
|
|
18
|
+
* - clause 7.1.6: that hash is signed with `algorithm` and the result is
|
|
19
|
+
* base64URL-encoded;
|
|
20
|
+
* - clause 7.1.7: pre-existing signatures are restored at the start of the
|
|
21
|
+
* list and the new entry is appended with its `value`;
|
|
22
|
+
* - clause 7.2: a countersignature is a signature object nested in the
|
|
23
|
+
* `signature` member of the signature it countersigns. The countersigned
|
|
24
|
+
* entry keeps its `value` in the signed view (7.2.1, 7.2.3, 7.2.4) and the
|
|
25
|
+
* other entries are removed (7.2.2);
|
|
26
|
+
* - clause 8.1: verification keeps only the entry under verification
|
|
27
|
+
* (8.1.1), removes its `value` (8.1.2), takes the key from the entry or a
|
|
28
|
+
* trusted source (8.1.3), canonicalizes (8.1.4), hashes (8.1.5) and
|
|
29
|
+
* validates (8.1.6).
|
|
30
|
+
*
|
|
31
|
+
* The signed message. The clause 7.1.6 example value
|
|
32
|
+
* (`F1Sj4V...`) and the clause 7.2.6 countersignature value (`b_7Xu5...`)
|
|
33
|
+
* are reproduced byte for byte only when the message given to the signing
|
|
34
|
+
* primitive is the UTF-8 text of the lowercase hex hash of clause 7.1.5
|
|
35
|
+
* (and 7.2.5). Ed25519 and Ed448 sign that message directly. RS*, PS* and
|
|
36
|
+
* ES* pass the same message to the JOSE primitive named in Table 1 (JWA
|
|
37
|
+
* [IETF RFC 7518] sections 3.3 to 3.5), which hashes it again with its own
|
|
38
|
+
* SHA-2 digest; clause 6.2.1 `hash_algorithm` notes that the signing
|
|
39
|
+
* process "sometimes uses an internal hashing algorithm". X.590 has no
|
|
40
|
+
* RSA or ECDSA example, so that part follows from the text and the Ed25519
|
|
41
|
+
* vectors rather than from a published vector.
|
|
42
|
+
*
|
|
43
|
+
* Trust. Clause 8.1.3 allows the key to be parsed from the entry or fetched
|
|
44
|
+
* from a trusted source, and clause 8.1.6 leaves "public key lookup methods"
|
|
45
|
+
* to policy. An embedded key with no trust anchor proves integrity only, so
|
|
46
|
+
* {@link verifyJss} requires caller-supplied trusted keys and uses the
|
|
47
|
+
* embedded key only with the explicit `allowEmbeddedKey` option, whose
|
|
48
|
+
* results carry `trusted: false`.
|
|
49
|
+
*
|
|
50
|
+
* The signature object shape is also fixed by the pinned draft schema,
|
|
51
|
+
* #/$defs/cyclonedx-jss_X590_2023_10-2.0/$defs/signatureObject. The
|
|
52
|
+
* `public_key` value is the base64 form of the DER SubjectPublicKeyInfo
|
|
53
|
+
* without the PEM armor (clause 6.2.1); the clause 7.1 examples store it
|
|
54
|
+
* without base64 padding, which the schema pattern also allows.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
constants,
|
|
59
|
+
createHash,
|
|
60
|
+
createPrivateKey,
|
|
61
|
+
createPublicKey,
|
|
62
|
+
sign as cryptoSign,
|
|
63
|
+
verify as cryptoVerify,
|
|
64
|
+
generateKeyPairSync,
|
|
65
|
+
KeyObject,
|
|
66
|
+
} from "node:crypto";
|
|
67
|
+
|
|
68
|
+
import {
|
|
69
|
+
BOM_20_ROOT_MEMBERS,
|
|
70
|
+
canonicalizeJson,
|
|
71
|
+
MAX_SIGNATURE_ENTRIES,
|
|
72
|
+
} from "./bomSigner.js";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Quantum-safe algorithm names from X.590 Table 1 (clause 6.2.2) that
|
|
76
|
+
* node:crypto cannot provide: every XMSS (IETF RFC 8391 section 5.3) and
|
|
77
|
+
* LMS (IETF RFC 8554 section 5.1) value of the table.
|
|
78
|
+
*/
|
|
79
|
+
export const UNSUPPORTED_JSS_ALGORITHMS = Object.freeze([
|
|
80
|
+
"XMSS-SHA2_10_256",
|
|
81
|
+
"XMSS-SHA2_16_256",
|
|
82
|
+
"XMSS-SHA2_20_256",
|
|
83
|
+
"LMS_SHA256_M32_H5",
|
|
84
|
+
"LMS_SHA256_M32_H10",
|
|
85
|
+
"LMS_SHA256_M32_H15",
|
|
86
|
+
"LMS_SHA256_M32_H20",
|
|
87
|
+
"LMS_SHA256_M32_H25",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Clause 6.2.2: JWA defines HS256, HS384 and HS512, but they "SHOULD NOT be
|
|
91
|
+
// used" and the sharing of their keys is out of scope, so JSS here refuses
|
|
92
|
+
// them.
|
|
93
|
+
const HMAC_ALGORITHMS = Object.freeze(["HS256", "HS384", "HS512"]);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The clause 6.2.1 `hash_algorithm` values accepted, by their IANA Named
|
|
97
|
+
* Information Hash Algorithm Registry names, mapped to the node:crypto
|
|
98
|
+
* digest. Anything else (md5, sha1, sha-224, unknown names) is refused when
|
|
99
|
+
* signing and fails verification: clause 6.2.1 asks for a "current and
|
|
100
|
+
* widely accepted hashing algorithm" defined in the IANA registry.
|
|
101
|
+
*/
|
|
102
|
+
export const JSS_HASH_ALGORITHMS = Object.freeze(
|
|
103
|
+
Object.assign(Object.create(null), {
|
|
104
|
+
"sha-256": "sha256",
|
|
105
|
+
"sha-384": "sha384",
|
|
106
|
+
"sha-512": "sha512",
|
|
107
|
+
"sha3-256": "sha3-256",
|
|
108
|
+
"sha3-384": "sha3-384",
|
|
109
|
+
"sha3-512": "sha3-512",
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// JWA [IETF RFC 7518] sections 3.3 and 3.5: a key of size 2048 bits or
|
|
114
|
+
// larger MUST be used with RS* and PS*.
|
|
115
|
+
const MIN_RSA_MODULUS_BITS = 2048;
|
|
116
|
+
|
|
117
|
+
const HASH_BYTES = Object.freeze({ sha256: 32, sha384: 48, sha512: 64 });
|
|
118
|
+
|
|
119
|
+
// X.590 Table 1 (clause 6.2.2) restricted to what node:crypto implements.
|
|
120
|
+
// `nodeHash` is the digest inside the JOSE primitive (Table 1 names it for
|
|
121
|
+
// every RS*, PS* and ES* entry); `defaultHash` is the clause 6.2.1
|
|
122
|
+
// `hash_algorithm` used when the caller names none.
|
|
123
|
+
const JSS_ALGORITHMS = new Map(
|
|
124
|
+
[
|
|
125
|
+
["RS256", "rsa", ["rsa"], "sha256", "sha-256"],
|
|
126
|
+
["RS384", "rsa", ["rsa"], "sha384", "sha-384"],
|
|
127
|
+
["RS512", "rsa", ["rsa"], "sha512", "sha-512"],
|
|
128
|
+
["PS256", "rsa-pss", ["rsa", "rsa-pss"], "sha256", "sha-256"],
|
|
129
|
+
["PS384", "rsa-pss", ["rsa", "rsa-pss"], "sha384", "sha-384"],
|
|
130
|
+
["PS512", "rsa-pss", ["rsa", "rsa-pss"], "sha512", "sha-512"],
|
|
131
|
+
["ES256", "ec", ["ec"], "sha256", "sha-256", "prime256v1"],
|
|
132
|
+
["ES384", "ec", ["ec"], "sha384", "sha-384", "secp384r1"],
|
|
133
|
+
["ES512", "ec", ["ec"], "sha512", "sha-512", "secp521r1"],
|
|
134
|
+
["Ed25519", "eddsa", ["ed25519"], null, "sha-512"],
|
|
135
|
+
["Ed448", "eddsa", ["ed448"], null, "sha-512"],
|
|
136
|
+
].map(([name, family, keyTypes, nodeHash, defaultHash, curve]) => [
|
|
137
|
+
name,
|
|
138
|
+
Object.freeze({ family, keyTypes, nodeHash, defaultHash, curve }),
|
|
139
|
+
]),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
/** JSS algorithm names this module signs and verifies. */
|
|
143
|
+
export const JSS_SIGNATURE_ALGORITHMS = Object.freeze([
|
|
144
|
+
...JSS_ALGORITHMS.keys(),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const CURVE_ALGORITHMS = Object.freeze({
|
|
148
|
+
prime256v1: "ES256",
|
|
149
|
+
secp384r1: "ES384",
|
|
150
|
+
secp521r1: "ES512",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/** Nested CycloneDX 2.0 members that carry `signatures` and are signed. */
|
|
154
|
+
const NESTED_SIGNATURE_TARGETS = Object.freeze([
|
|
155
|
+
"inventories",
|
|
156
|
+
"compositions",
|
|
157
|
+
"annotations",
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
const BOM_ROOT_MEMBER_SET = new Set(BOM_20_ROOT_MEMBERS);
|
|
161
|
+
const BOM_IDENTITY_MEMBERS = ["bomFormat", "specFormat", "specVersion"];
|
|
162
|
+
|
|
163
|
+
const isPlainObject = (value) =>
|
|
164
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
165
|
+
|
|
166
|
+
const printable = (value) => {
|
|
167
|
+
if (typeof value === "string") {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return JSON.stringify(value) ?? String(value);
|
|
172
|
+
} catch {
|
|
173
|
+
return `<${typeof value}>`;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const stripPadding = (text) => {
|
|
178
|
+
let end = text.length;
|
|
179
|
+
while (end > 0 && text[end - 1] === "=") {
|
|
180
|
+
end--;
|
|
181
|
+
}
|
|
182
|
+
return text.slice(0, end);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
let ed448Available;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Whether this runtime's crypto provides Ed448. Bun links BoringSSL, which
|
|
189
|
+
* has no Ed448, so key generation and key loading fail there with an
|
|
190
|
+
* OpenSSL error; this check turns that into a clear message.
|
|
191
|
+
*
|
|
192
|
+
* @returns {boolean} true when Ed448 keys can be created
|
|
193
|
+
*/
|
|
194
|
+
export function isEd448Available() {
|
|
195
|
+
if (ed448Available === undefined) {
|
|
196
|
+
try {
|
|
197
|
+
generateKeyPairSync("ed448");
|
|
198
|
+
ed448Available = true;
|
|
199
|
+
} catch {
|
|
200
|
+
ed448Available = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return ed448Available;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function algorithmSpec(algorithm) {
|
|
207
|
+
const spec =
|
|
208
|
+
typeof algorithm === "string" ? JSS_ALGORITHMS.get(algorithm) : undefined;
|
|
209
|
+
if (spec) {
|
|
210
|
+
if (algorithm === "Ed448" && !isEd448Available()) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
"JSS algorithm Ed448 is not available in this runtime, whose crypto library has no Ed448 (Bun uses BoringSSL). Use Ed25519 or an ES* algorithm, or run under Node.js or Deno.",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return spec;
|
|
216
|
+
}
|
|
217
|
+
if (UNSUPPORTED_JSS_ALGORITHMS.includes(algorithm)) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`JSS algorithm ${algorithm} is not supported: quantum-safe XMSS/LMS signing is not available in node:crypto.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (HMAC_ALGORITHMS.includes(algorithm)) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`JSS algorithm ${algorithm} is not supported: X.590 clause 6.2.2 says HMAC algorithms SHOULD NOT be used, and cdxgen does not sign or verify them for JSS.`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Unsupported JSS algorithm: ${printable(algorithm)}. Supported algorithms: ${JSS_SIGNATURE_ALGORITHMS.join(", ")}.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function nodeHashFor(hashAlgorithm) {
|
|
233
|
+
if (
|
|
234
|
+
typeof hashAlgorithm !== "string" ||
|
|
235
|
+
!Object.hasOwn(JSS_HASH_ALGORITHMS, hashAlgorithm)
|
|
236
|
+
) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Unsupported JSS hash_algorithm: ${printable(hashAlgorithm)}. Supported: ${Object.keys(JSS_HASH_ALGORITHMS).join(", ")}.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return JSS_HASH_ALGORITHMS[hashAlgorithm];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function describeKey(key) {
|
|
245
|
+
if (!(key instanceof KeyObject)) {
|
|
246
|
+
return "not a key";
|
|
247
|
+
}
|
|
248
|
+
if (key.type === "secret") {
|
|
249
|
+
return "a shared secret";
|
|
250
|
+
}
|
|
251
|
+
const details = key.asymmetricKeyDetails || {};
|
|
252
|
+
if (details.namedCurve) {
|
|
253
|
+
return `${key.asymmetricKeyType} (${details.namedCurve})`;
|
|
254
|
+
}
|
|
255
|
+
if (details.modulusLength) {
|
|
256
|
+
return `${key.asymmetricKeyType} (${details.modulusLength} bits)`;
|
|
257
|
+
}
|
|
258
|
+
return String(key.asymmetricKeyType);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function describeRequirement(spec) {
|
|
262
|
+
if (spec.curve) {
|
|
263
|
+
return `an ec (${spec.curve}) key`;
|
|
264
|
+
}
|
|
265
|
+
if (spec.family === "rsa" || spec.family === "rsa-pss") {
|
|
266
|
+
return `an ${spec.keyTypes.join(" or ")} key of at least ${MIN_RSA_MODULUS_BITS} bits`;
|
|
267
|
+
}
|
|
268
|
+
return `an ${spec.keyTypes[0]} key`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Checks that a key fits a JSS algorithm, the binding X.590 Table 1 (clause
|
|
273
|
+
* 6.2.2) defines through JWA [IETF RFC 7518] sections 3.3 to 3.5 and
|
|
274
|
+
* [IETF RFC 8037]: RS* need an rsa key, PS* an rsa or rsa-pss key, both of
|
|
275
|
+
* at least 2048 bits; ES256, ES384 and ES512 need an ec key on P-256, P-384
|
|
276
|
+
* and P-521 exactly; Ed25519 and Ed448 need that key type. Sign and verify
|
|
277
|
+
* both call it, so a mislabelled entry fails before any signature math.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} algorithm - JSS algorithm name
|
|
280
|
+
* @param {KeyObject} key - Public or private key
|
|
281
|
+
* @throws {Error} When the key does not fit the algorithm
|
|
282
|
+
*/
|
|
283
|
+
export function assertJssKeyMatchesAlgorithm(algorithm, key) {
|
|
284
|
+
const spec = algorithmSpec(algorithm);
|
|
285
|
+
let matches =
|
|
286
|
+
key instanceof KeyObject &&
|
|
287
|
+
key.type !== "secret" &&
|
|
288
|
+
spec.keyTypes.includes(key.asymmetricKeyType);
|
|
289
|
+
const details = matches ? key.asymmetricKeyDetails || {} : {};
|
|
290
|
+
if (matches && spec.curve) {
|
|
291
|
+
matches = details.namedCurve === spec.curve;
|
|
292
|
+
}
|
|
293
|
+
if (matches && (spec.family === "rsa" || spec.family === "rsa-pss")) {
|
|
294
|
+
matches = (details.modulusLength || 0) >= MIN_RSA_MODULUS_BITS;
|
|
295
|
+
}
|
|
296
|
+
// An rsa-pss key may restrict its parameters (IETF RFC 4055); they must
|
|
297
|
+
// allow the Table 1 digest, MGF1 with the same digest, and a salt the size
|
|
298
|
+
// of the digest (JWA section 3.5).
|
|
299
|
+
if (matches && key.asymmetricKeyType === "rsa-pss") {
|
|
300
|
+
matches =
|
|
301
|
+
(details.hashAlgorithm === undefined ||
|
|
302
|
+
details.hashAlgorithm === spec.nodeHash) &&
|
|
303
|
+
(details.mgf1HashAlgorithm === undefined ||
|
|
304
|
+
details.mgf1HashAlgorithm === spec.nodeHash) &&
|
|
305
|
+
(details.saltLength === undefined ||
|
|
306
|
+
details.saltLength <= HASH_BYTES[spec.nodeHash]);
|
|
307
|
+
}
|
|
308
|
+
if (!matches) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`JSS algorithm ${algorithm} requires ${describeRequirement(spec)}, but the key is ${describeKey(key)}.`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Picks the JSS algorithm for a key: ES256, ES384 or ES512 by curve, RS256
|
|
317
|
+
* for rsa, PS256 for rsa-pss, Ed25519 or Ed448 for those key types.
|
|
318
|
+
*
|
|
319
|
+
* @param {KeyObject} key - Public or private key
|
|
320
|
+
* @returns {string} JSS algorithm name
|
|
321
|
+
* @throws {Error} For a key type no JSS algorithm here supports
|
|
322
|
+
*/
|
|
323
|
+
export function selectJssAlgorithmForKey(key) {
|
|
324
|
+
const type = key?.asymmetricKeyType;
|
|
325
|
+
if (type === "ec") {
|
|
326
|
+
const curve = key.asymmetricKeyDetails?.namedCurve;
|
|
327
|
+
if (Object.hasOwn(CURVE_ALGORITHMS, curve)) {
|
|
328
|
+
return CURVE_ALGORITHMS[curve];
|
|
329
|
+
}
|
|
330
|
+
} else if (type === "rsa") {
|
|
331
|
+
return "RS256";
|
|
332
|
+
} else if (type === "rsa-pss") {
|
|
333
|
+
return "PS256";
|
|
334
|
+
} else if (type === "ed25519") {
|
|
335
|
+
return "Ed25519";
|
|
336
|
+
} else if (type === "ed448") {
|
|
337
|
+
return "Ed448";
|
|
338
|
+
}
|
|
339
|
+
throw new Error(
|
|
340
|
+
`No JSS algorithm supports this key (${describeKey(key)}). Use an rsa, rsa-pss, ec (P-256, P-384, P-521), ed25519 or ed448 key.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The base64 (unpadded, as in the X.590 clause 7.1 examples) DER SPKI. */
|
|
345
|
+
function spkiBase64(publicKey) {
|
|
346
|
+
return stripPadding(
|
|
347
|
+
publicKey.export({ type: "spki", format: "der" }).toString("base64"),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Parse a `public_key` value (padded or unpadded base64 DER SPKI). */
|
|
352
|
+
function parseSpkiBase64(value) {
|
|
353
|
+
const text = stripPadding(value.replace(/\s+/g, ""));
|
|
354
|
+
const der = Buffer.from(text, "base64");
|
|
355
|
+
if (stripPadding(der.toString("base64")) !== text) {
|
|
356
|
+
throw new Error("The public_key is not canonical base64.");
|
|
357
|
+
}
|
|
358
|
+
return createPublicKey({ key: der, format: "der", type: "spki" });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function toPublicKey(key) {
|
|
362
|
+
if (key instanceof KeyObject) {
|
|
363
|
+
if (key.type === "secret") {
|
|
364
|
+
throw new Error(
|
|
365
|
+
"A shared secret cannot verify JSS signatures; pass a public key.",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return key.type === "private" ? createPublicKey(key) : key;
|
|
369
|
+
}
|
|
370
|
+
return createPublicKey(key);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function toPrivateKey(key) {
|
|
374
|
+
if (key instanceof KeyObject) {
|
|
375
|
+
if (key.type !== "private") {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`JSS signing needs a private key, not a ${key.type} key.`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return key;
|
|
381
|
+
}
|
|
382
|
+
if (key === undefined || key === null) {
|
|
383
|
+
throw new Error("JSS signing needs a private key.");
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
return createPrivateKey(key);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
throw new Error(`Unable to load the JSS signing key: ${err.message}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The bytes handed to the signing primitive: the UTF-8 text of the lowercase
|
|
394
|
+
* hex hash of the JCS form (clauses 7.1.5 and 7.1.6; the clause 7.1.6 and
|
|
395
|
+
* 7.2.6 example values verify only over this message).
|
|
396
|
+
*/
|
|
397
|
+
function signedMessage(jcs, hashAlgorithm) {
|
|
398
|
+
const digest = createHash(nodeHashFor(hashAlgorithm))
|
|
399
|
+
.update(jcs, "utf8")
|
|
400
|
+
.digest("hex");
|
|
401
|
+
return Buffer.from(digest, "utf8");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function primitiveOptions(spec, key) {
|
|
405
|
+
if (spec.family === "rsa") {
|
|
406
|
+
// JWA section 3.3: RSASSA-PKCS1-v1_5 with the Table 1 SHA-2 digest.
|
|
407
|
+
return { key, padding: constants.RSA_PKCS1_PADDING };
|
|
408
|
+
}
|
|
409
|
+
if (spec.family === "rsa-pss") {
|
|
410
|
+
// JWA section 3.5: MGF1 uses the signing digest (node's default) and the
|
|
411
|
+
// salt is the size of the digest output.
|
|
412
|
+
return {
|
|
413
|
+
key,
|
|
414
|
+
padding: constants.RSA_PKCS1_PSS_PADDING,
|
|
415
|
+
saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (spec.family === "ec") {
|
|
419
|
+
// JWA section 3.4: the signature is the fixed-size R || S pair.
|
|
420
|
+
return { key, dsaEncoding: "ieee-p1363" };
|
|
421
|
+
}
|
|
422
|
+
// Ed25519/Ed448 ([IETF RFC 8037], [IETF RFC 8032]) sign the message itself.
|
|
423
|
+
return key;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function decodeValue(value) {
|
|
427
|
+
if (typeof value !== "string" || !value.length) {
|
|
428
|
+
throw new Error("The signature value is missing (X.590 clause 8.1.2).");
|
|
429
|
+
}
|
|
430
|
+
const text = stripPadding(value);
|
|
431
|
+
const bytes = Buffer.from(text, "base64url");
|
|
432
|
+
if (bytes.toString("base64url") !== text) {
|
|
433
|
+
throw new Error("The signature value is not canonical base64url.");
|
|
434
|
+
}
|
|
435
|
+
return bytes;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Refuses values that JCS (IETF RFC 8785 sections 3.2.2.3 and 3.2.2.2)
|
|
440
|
+
* cannot represent: numbers that are not finite and strings holding an
|
|
441
|
+
* unpaired UTF-16 surrogate. JSON.stringify would silently turn the first
|
|
442
|
+
* into null and escape the second, giving a canonical form other JCS
|
|
443
|
+
* implementations reject.
|
|
444
|
+
*
|
|
445
|
+
* @param {*} value - Value about to be canonicalized
|
|
446
|
+
* @param {string} [path] - JSON path for the error message
|
|
447
|
+
* @throws {Error} For a value JCS cannot represent
|
|
448
|
+
*/
|
|
449
|
+
function assertJcsRepresentable(value, path = "$") {
|
|
450
|
+
const stack = [[value, path]];
|
|
451
|
+
while (stack.length) {
|
|
452
|
+
const [current, where] = stack.pop();
|
|
453
|
+
if (typeof current === "number") {
|
|
454
|
+
if (!Number.isFinite(current)) {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`JCS cannot represent the number at ${where} (${current}).`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
} else if (typeof current === "string") {
|
|
460
|
+
if (!current.isWellFormed()) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`JCS cannot represent the string at ${where}: it holds an unpaired UTF-16 surrogate.`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
} else if (Array.isArray(current)) {
|
|
466
|
+
for (let index = current.length - 1; index >= 0; index--) {
|
|
467
|
+
stack.push([current[index], `${where}[${index}]`]);
|
|
468
|
+
}
|
|
469
|
+
} else if (current !== null && typeof current === "object") {
|
|
470
|
+
for (const key of Object.keys(current)) {
|
|
471
|
+
if (!key.isWellFormed()) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`JCS cannot represent a member name under ${where}: it holds an unpaired UTF-16 surrogate.`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
stack.push([current[key], `${where}.${key}`]);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Returns a function producing the JCS text of `{ ...content, signatures:
|
|
484
|
+
* [entry] }`, canonicalizing the content once (clause 7.1.4, 8.1.4).
|
|
485
|
+
*/
|
|
486
|
+
function viewSerializer(content) {
|
|
487
|
+
assertJcsRepresentable(content);
|
|
488
|
+
const before = [];
|
|
489
|
+
const after = [];
|
|
490
|
+
for (const name of Object.keys(content).sort()) {
|
|
491
|
+
const member = canonicalizeJson(content[name]);
|
|
492
|
+
if (member !== undefined) {
|
|
493
|
+
(name < "signatures" ? before : after).push(
|
|
494
|
+
`${JSON.stringify(name)}:${member}`,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return (entry) => {
|
|
499
|
+
assertJcsRepresentable(entry, "$.signatures");
|
|
500
|
+
return `{${[...before, `"signatures":[${canonicalizeJson(entry)}]`, ...after].join(",")}}`;
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function withoutValueAndCountersignature(entry) {
|
|
505
|
+
// Clause 8.1.2 removes the value. A nested `signature` was added after
|
|
506
|
+
// this entry was signed (clauses 7.2.3, 7.2.7), so it is not part of what
|
|
507
|
+
// this entry covers; it is verified on its own (clause 7.2).
|
|
508
|
+
const { value: _value, signature: _countersignature, ...rest } = entry;
|
|
509
|
+
return rest;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function readSignatures(document) {
|
|
513
|
+
if (!isPlainObject(document)) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
"JSS signs and verifies JSON objects only (X.590 clause 7).",
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const { signatures } = document;
|
|
519
|
+
if (signatures === undefined) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
if (!Array.isArray(signatures)) {
|
|
523
|
+
throw new Error("The signatures member must be a list (X.590 clause 6).");
|
|
524
|
+
}
|
|
525
|
+
return signatures;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Flattens each root entry and its countersignature chain (clause 7.2) into
|
|
530
|
+
* the list of signature objects to verify.
|
|
531
|
+
*/
|
|
532
|
+
function signatureChain(entry) {
|
|
533
|
+
const chain = [entry];
|
|
534
|
+
let current = entry;
|
|
535
|
+
while (isPlainObject(current) && current.signature !== undefined) {
|
|
536
|
+
current = current.signature;
|
|
537
|
+
chain.push(current);
|
|
538
|
+
if (chain.length > MAX_SIGNATURE_ENTRIES) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
`A countersignature chain holds at most ${MAX_SIGNATURE_ENTRIES} signature objects.`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return chain;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* The single signature object of the signed view for depth `depth` of a
|
|
549
|
+
* chain: the ancestors keep their values (clauses 7.2.1, 7.2.4), the object
|
|
550
|
+
* under verification loses its value and any later countersignature.
|
|
551
|
+
*/
|
|
552
|
+
function chainView(chain, depth) {
|
|
553
|
+
let node = withoutValueAndCountersignature(chain[depth]);
|
|
554
|
+
for (let index = depth - 1; index >= 0; index--) {
|
|
555
|
+
node = { ...chain[index], signature: node };
|
|
556
|
+
}
|
|
557
|
+
return node;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function signEntry(serialize, entry, hashAlgorithm, spec, key) {
|
|
561
|
+
const message = signedMessage(serialize(entry), hashAlgorithm);
|
|
562
|
+
// Clause 7.1.6: base64URL without padding (node's base64url has none).
|
|
563
|
+
return cryptoSign(
|
|
564
|
+
spec.nodeHash,
|
|
565
|
+
message,
|
|
566
|
+
primitiveOptions(spec, key),
|
|
567
|
+
).toString("base64url");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function prepareSigning(algorithm, privateKey, hashAlgorithm) {
|
|
571
|
+
const spec = algorithmSpec(algorithm);
|
|
572
|
+
const hashName = hashAlgorithm ?? spec.defaultHash;
|
|
573
|
+
nodeHashFor(hashName);
|
|
574
|
+
const key = toPrivateKey(privateKey);
|
|
575
|
+
assertJssKeyMatchesAlgorithm(algorithm, key);
|
|
576
|
+
const entry = {
|
|
577
|
+
hash_algorithm: hashName,
|
|
578
|
+
algorithm,
|
|
579
|
+
public_key: spkiBase64(createPublicKey(key)),
|
|
580
|
+
};
|
|
581
|
+
return { spec, hashName, key, entry };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Sign a JSON object with JSS (X.590 clause 7.1). Existing entries stay
|
|
586
|
+
* valid: they are removed while the new entry is computed (clause 7.1.2)
|
|
587
|
+
* and restored ahead of it (clause 7.1.7). The input is not mutated.
|
|
588
|
+
*
|
|
589
|
+
* @param {object} document - The JSON object to sign
|
|
590
|
+
* @param {object} options
|
|
591
|
+
* @param {string} options.algorithm - JSS algorithm name (clause 6.2.2)
|
|
592
|
+
* @param {KeyObject|string|Buffer} options.privateKey - Private key (KeyObject or PEM)
|
|
593
|
+
* @param {string} [options.hashAlgorithm] - Clause 6.2.1 hash; one of {@link JSS_HASH_ALGORITHMS}
|
|
594
|
+
* @returns {object} A new object carrying the `signatures` array
|
|
595
|
+
*/
|
|
596
|
+
export function signJss(
|
|
597
|
+
document,
|
|
598
|
+
{ algorithm, privateKey, hashAlgorithm } = {},
|
|
599
|
+
) {
|
|
600
|
+
const existing = readSignatures(document);
|
|
601
|
+
if (existing.length >= MAX_SIGNATURE_ENTRIES) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`Cannot append another JSS signature: at most ${MAX_SIGNATURE_ENTRIES} are supported.`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
const { spec, hashName, key, entry } = prepareSigning(
|
|
607
|
+
algorithm,
|
|
608
|
+
privateKey,
|
|
609
|
+
hashAlgorithm,
|
|
610
|
+
);
|
|
611
|
+
// Clause 7.1.2: temporarily remove existing signatures.
|
|
612
|
+
const { signatures: _existing, ...rest } = document;
|
|
613
|
+
// Clauses 7.1.3-7.1.6: add the entry without its value, canonicalize,
|
|
614
|
+
// hash, sign the hex hash and base64URL-encode.
|
|
615
|
+
const value = signEntry(viewSerializer(rest), entry, hashName, spec, key);
|
|
616
|
+
// Clause 7.1.7: existing signatures first, the new entry appended.
|
|
617
|
+
return { ...rest, signatures: [...existing, { ...entry, value }] };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Countersign a JSS signature (X.590 clause 7.2): the new signature object
|
|
622
|
+
* goes into the `signature` member at the end of the countersignature chain
|
|
623
|
+
* of `signatures[index]`, and covers the document plus that entry and its
|
|
624
|
+
* chain, values included. Other entries are left out of the signed view
|
|
625
|
+
* (clause 7.2.2) and restored ahead of the countersigned entry (clause
|
|
626
|
+
* 7.2.7). The input is not mutated.
|
|
627
|
+
*
|
|
628
|
+
* @param {object} document - The signed JSON object
|
|
629
|
+
* @param {object} options
|
|
630
|
+
* @param {string} options.algorithm - JSS algorithm name (clause 6.2.2)
|
|
631
|
+
* @param {KeyObject|string|Buffer} options.privateKey - Private key (KeyObject or PEM)
|
|
632
|
+
* @param {string} [options.hashAlgorithm] - Clause 6.2.1 hash
|
|
633
|
+
* @param {number} [options.index] - Entry to countersign; required when there are several
|
|
634
|
+
* @returns {object} A new object carrying the countersigned `signatures` array
|
|
635
|
+
*/
|
|
636
|
+
export function countersignJss(
|
|
637
|
+
document,
|
|
638
|
+
{ algorithm, privateKey, hashAlgorithm, index } = {},
|
|
639
|
+
) {
|
|
640
|
+
const existing = readSignatures(document);
|
|
641
|
+
if (!existing.length) {
|
|
642
|
+
throw new Error("There is no JSS signature to countersign.");
|
|
643
|
+
}
|
|
644
|
+
// With several entries the caller must say which one is countersigned.
|
|
645
|
+
const target = index ?? (existing.length === 1 ? 0 : undefined);
|
|
646
|
+
if (!Number.isInteger(target) || target < 0 || target >= existing.length) {
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Name the JSS signature to countersign: the document has ${existing.length} entries.`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (!isPlainObject(existing[target])) {
|
|
652
|
+
throw new Error("The countersigned entry is not a JSS signature object.");
|
|
653
|
+
}
|
|
654
|
+
const chain = signatureChain(existing[target]);
|
|
655
|
+
if (chain.length >= MAX_SIGNATURE_ENTRIES) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`Cannot countersign: a chain holds at most ${MAX_SIGNATURE_ENTRIES} signature objects.`,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const { spec, hashName, key, entry } = prepareSigning(
|
|
661
|
+
algorithm,
|
|
662
|
+
privateKey,
|
|
663
|
+
hashAlgorithm,
|
|
664
|
+
);
|
|
665
|
+
const attach = (countersignature) => {
|
|
666
|
+
let node = countersignature;
|
|
667
|
+
for (let depth = chain.length - 1; depth >= 0; depth--) {
|
|
668
|
+
node = { ...chain[depth], signature: node };
|
|
669
|
+
}
|
|
670
|
+
return node;
|
|
671
|
+
};
|
|
672
|
+
const { signatures: _existing, ...rest } = document;
|
|
673
|
+
// Clauses 7.2.2-7.2.6: only the countersigned entry, which keeps its
|
|
674
|
+
// value, with the new object (without value) nested in it.
|
|
675
|
+
const value = signEntry(
|
|
676
|
+
viewSerializer(rest),
|
|
677
|
+
attach(entry),
|
|
678
|
+
hashName,
|
|
679
|
+
spec,
|
|
680
|
+
key,
|
|
681
|
+
);
|
|
682
|
+
// Clause 7.2.7: the other entries first, then the countersigned one.
|
|
683
|
+
return {
|
|
684
|
+
...rest,
|
|
685
|
+
signatures: [
|
|
686
|
+
...existing.filter((_entry, position) => position !== target),
|
|
687
|
+
attach({ ...entry, value }),
|
|
688
|
+
],
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function loadTrustedKeys({ publicKey, publicKeys } = {}) {
|
|
693
|
+
const inputs = [];
|
|
694
|
+
if (publicKey !== undefined && publicKey !== null) {
|
|
695
|
+
inputs.push(publicKey);
|
|
696
|
+
}
|
|
697
|
+
if (Array.isArray(publicKeys)) {
|
|
698
|
+
inputs.push(...publicKeys);
|
|
699
|
+
} else if (publicKeys !== undefined && publicKeys !== null) {
|
|
700
|
+
inputs.push(publicKeys);
|
|
701
|
+
}
|
|
702
|
+
return inputs.map((input) => {
|
|
703
|
+
const key = toPublicKey(input);
|
|
704
|
+
return { key, der: key.export({ type: "spki", format: "der" }) };
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function requireTrust(trustedKeys, allowEmbeddedKey) {
|
|
709
|
+
if (!trustedKeys.length && !allowEmbeddedKey) {
|
|
710
|
+
throw new Error(
|
|
711
|
+
"JSS verification needs a trusted publicKey (or publicKeys). Pass allowEmbeddedKey: true for an integrity-only check with the keys embedded in the document.",
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Checks one signature object of a chain against the trusted keys.
|
|
718
|
+
*
|
|
719
|
+
* Status values: `verified` (a trusted key verifies it), `failed` (it names
|
|
720
|
+
* a trusted key in `public_key` and does not verify with it, or it is
|
|
721
|
+
* malformed and names a trusted key), `untrusted` (no trusted key verifies
|
|
722
|
+
* it and it does not name one), `integrity-only` (only its embedded key
|
|
723
|
+
* verifies it, with `allowEmbeddedKey`).
|
|
724
|
+
*/
|
|
725
|
+
function checkSignatureObject(item, serialize, view, trustedKeys, options) {
|
|
726
|
+
const result = {
|
|
727
|
+
algorithm: isPlainObject(item) ? item.algorithm : undefined,
|
|
728
|
+
hashAlgorithm: isPlainObject(item) ? item.hash_algorithm : undefined,
|
|
729
|
+
status: "untrusted",
|
|
730
|
+
valid: false,
|
|
731
|
+
trusted: false,
|
|
732
|
+
claimsTrustedKey: false,
|
|
733
|
+
keyIndex: undefined,
|
|
734
|
+
reason: undefined,
|
|
735
|
+
};
|
|
736
|
+
if (!isPlainObject(item)) {
|
|
737
|
+
result.status = "failed";
|
|
738
|
+
result.reason = "Not a JSS signature object (X.590 clause 8.1.2).";
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
// Which trusted key does the entry name? The embedded key is decoded and
|
|
742
|
+
// its DER compared byte for byte with each trusted key, so a re-encoded
|
|
743
|
+
// copy of a trusted key (padding, line breaks, whitespace) still counts as
|
|
744
|
+
// naming it. A public_key that does not decode to a key names no trusted
|
|
745
|
+
// key; the entry is untrusted (the X.590 clause 7.1.6 example carries such
|
|
746
|
+
// a placeholder signer), or failed under allowEmbeddedKey.
|
|
747
|
+
const embedded = item.public_key;
|
|
748
|
+
let candidates = trustedKeys.map((trusted, keyIndex) => ({
|
|
749
|
+
...trusted,
|
|
750
|
+
keyIndex,
|
|
751
|
+
}));
|
|
752
|
+
if (embedded !== undefined) {
|
|
753
|
+
let embeddedDer;
|
|
754
|
+
try {
|
|
755
|
+
if (typeof embedded !== "string") {
|
|
756
|
+
throw new Error("The public_key must be a string.");
|
|
757
|
+
}
|
|
758
|
+
embeddedDer = parseSpkiBase64(embedded).export({
|
|
759
|
+
type: "spki",
|
|
760
|
+
format: "der",
|
|
761
|
+
});
|
|
762
|
+
} catch (err) {
|
|
763
|
+
result.status = options.allowEmbeddedKey ? "failed" : "untrusted";
|
|
764
|
+
result.reason = `The public_key is not a usable public key: ${err.message}`;
|
|
765
|
+
return result;
|
|
766
|
+
}
|
|
767
|
+
candidates = candidates.filter((trusted) =>
|
|
768
|
+
trusted.der.equals(embeddedDer),
|
|
769
|
+
);
|
|
770
|
+
result.claimsTrustedKey = candidates.length > 0;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
const spec = algorithmSpec(item.algorithm);
|
|
774
|
+
nodeHashFor(item.hash_algorithm);
|
|
775
|
+
const signature = decodeValue(item.value);
|
|
776
|
+
let message;
|
|
777
|
+
const messageBytes = () => {
|
|
778
|
+
message ??= signedMessage(serialize(view), item.hash_algorithm);
|
|
779
|
+
return message;
|
|
780
|
+
};
|
|
781
|
+
const reasons = [];
|
|
782
|
+
for (const candidate of candidates) {
|
|
783
|
+
try {
|
|
784
|
+
assertJssKeyMatchesAlgorithm(item.algorithm, candidate.key);
|
|
785
|
+
if (
|
|
786
|
+
cryptoVerify(
|
|
787
|
+
spec.nodeHash,
|
|
788
|
+
messageBytes(),
|
|
789
|
+
primitiveOptions(spec, candidate.key),
|
|
790
|
+
signature,
|
|
791
|
+
)
|
|
792
|
+
) {
|
|
793
|
+
return {
|
|
794
|
+
...result,
|
|
795
|
+
status: "verified",
|
|
796
|
+
valid: true,
|
|
797
|
+
trusted: true,
|
|
798
|
+
keyIndex: candidate.keyIndex,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
reasons.push(
|
|
802
|
+
`The ${item.algorithm} signature does not verify with trusted key #${candidate.keyIndex + 1}: the signed content or signature metadata changed, or a different key made it.`,
|
|
803
|
+
);
|
|
804
|
+
} catch (err) {
|
|
805
|
+
reasons.push(err.message);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (
|
|
809
|
+
!result.claimsTrustedKey &&
|
|
810
|
+
options.allowEmbeddedKey &&
|
|
811
|
+
embedded !== undefined
|
|
812
|
+
) {
|
|
813
|
+
const key = parseSpkiBase64(item.public_key);
|
|
814
|
+
assertJssKeyMatchesAlgorithm(item.algorithm, key);
|
|
815
|
+
const ok = cryptoVerify(
|
|
816
|
+
spec.nodeHash,
|
|
817
|
+
messageBytes(),
|
|
818
|
+
primitiveOptions(spec, key),
|
|
819
|
+
signature,
|
|
820
|
+
);
|
|
821
|
+
return {
|
|
822
|
+
...result,
|
|
823
|
+
status: ok ? "integrity-only" : "failed",
|
|
824
|
+
valid: ok,
|
|
825
|
+
reason: ok
|
|
826
|
+
? "Verified with the key embedded in the entry, which no trusted key vouches for."
|
|
827
|
+
: "The signature does not verify with its embedded public_key.",
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (result.claimsTrustedKey) {
|
|
831
|
+
result.status = "failed";
|
|
832
|
+
}
|
|
833
|
+
result.reason =
|
|
834
|
+
[...new Set(reasons)].join(" ") ||
|
|
835
|
+
(embedded !== undefined
|
|
836
|
+
? "Signed by a key that is not one of the trusted keys."
|
|
837
|
+
: "No trusted key verifies this entry.");
|
|
838
|
+
return result;
|
|
839
|
+
} catch (err) {
|
|
840
|
+
result.status =
|
|
841
|
+
result.claimsTrustedKey || options.allowEmbeddedKey
|
|
842
|
+
? "failed"
|
|
843
|
+
: "untrusted";
|
|
844
|
+
result.reason = err?.message || String(err);
|
|
845
|
+
return result;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Verify the JSS signatures of a JSON object (X.590 clauses 8.1 and 7.2).
|
|
851
|
+
*
|
|
852
|
+
* Each entry is verified on its own view: the other entries are removed
|
|
853
|
+
* (clause 8.1.1) and the entry loses only its `value` (clause 8.1.2) and any
|
|
854
|
+
* nested countersignature, so every other member (`signee`, `valid_until`,
|
|
855
|
+
* `revoked`, ...) is covered. Each countersignature in an entry's `signature`
|
|
856
|
+
* chain is verified on the clause 7.2 view and reported with `depth` > 0.
|
|
857
|
+
*
|
|
858
|
+
* Trust comes from the caller (clause 8.1.3 "fetch it from a trusted
|
|
859
|
+
* source"). An entry whose `public_key` is a trusted key is checked with that
|
|
860
|
+
* key only; an entry without `public_key` (thumbprint, certificate chain,
|
|
861
|
+
* certificate URL) is checked with every trusted key. The embedded key is
|
|
862
|
+
* used only with `allowEmbeddedKey`, and such results carry
|
|
863
|
+
* `trusted: false`.
|
|
864
|
+
*
|
|
865
|
+
* Policy: with trusted keys, the object verifies when at least one
|
|
866
|
+
* signature object verifies with a trusted key and no signature object that
|
|
867
|
+
* names a trusted key fails. With `requireAll`, every signature object must
|
|
868
|
+
* verify with a trusted key, and with `requireEveryKey` every trusted key
|
|
869
|
+
* must verify one of them. With `allowEmbeddedKey` and no trusted keys,
|
|
870
|
+
* every signature object must verify with its embedded key, and the result
|
|
871
|
+
* is `trusted: false`.
|
|
872
|
+
*
|
|
873
|
+
* @param {object} document - The signed JSON object
|
|
874
|
+
* @param {object} options
|
|
875
|
+
* @param {KeyObject|string|Buffer} [options.publicKey] - A trusted public key
|
|
876
|
+
* @param {Array<KeyObject|string|Buffer>} [options.publicKeys] - Trusted public keys
|
|
877
|
+
* @param {boolean} [options.allowEmbeddedKey] - Fall back to each entry's embedded public_key for an integrity-only check (results are `trusted: false`)
|
|
878
|
+
* @param {boolean} [options.requireAll] - Every signature object must verify with a trusted key
|
|
879
|
+
* @param {boolean} [options.requireEveryKey] - With requireAll, every trusted key must verify a signature object
|
|
880
|
+
* @returns {{valid: boolean, trusted: boolean, reasons: string[], keysMatched: boolean[], results: Array<{index: number, depth: number, path: string, algorithm: any, hashAlgorithm: any, status: string, valid: boolean, trusted: boolean, claimsTrustedKey: boolean, keyIndex: (number|undefined), reason: (string|undefined)}>}}
|
|
881
|
+
* @throws {Error} When neither a trusted key nor allowEmbeddedKey is given
|
|
882
|
+
*/
|
|
883
|
+
export function verifyJss(document, options = {}) {
|
|
884
|
+
const trustedKeys = loadTrustedKeys(options);
|
|
885
|
+
const allowEmbeddedKey = Boolean(options.allowEmbeddedKey);
|
|
886
|
+
requireTrust(trustedKeys, allowEmbeddedKey);
|
|
887
|
+
const report = {
|
|
888
|
+
valid: false,
|
|
889
|
+
trusted: false,
|
|
890
|
+
reasons: [],
|
|
891
|
+
keysMatched: trustedKeys.map(() => false),
|
|
892
|
+
results: [],
|
|
893
|
+
};
|
|
894
|
+
let signatures;
|
|
895
|
+
try {
|
|
896
|
+
signatures = readSignatures(document);
|
|
897
|
+
if (!signatures.length) {
|
|
898
|
+
throw new Error("The object has no JSS signatures.");
|
|
899
|
+
}
|
|
900
|
+
if (signatures.length > MAX_SIGNATURE_ENTRIES) {
|
|
901
|
+
throw new Error(
|
|
902
|
+
`The signatures list has ${signatures.length} entries; at most ${MAX_SIGNATURE_ENTRIES} are supported.`,
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
} catch (err) {
|
|
906
|
+
report.reasons.push(err.message);
|
|
907
|
+
return report;
|
|
908
|
+
}
|
|
909
|
+
const { signatures: _removed, ...rest } = document;
|
|
910
|
+
let serialize;
|
|
911
|
+
try {
|
|
912
|
+
serialize = viewSerializer(rest);
|
|
913
|
+
} catch (err) {
|
|
914
|
+
report.reasons.push(err.message);
|
|
915
|
+
return report;
|
|
916
|
+
}
|
|
917
|
+
let total = 0;
|
|
918
|
+
for (const [index, entry] of signatures.entries()) {
|
|
919
|
+
let chain;
|
|
920
|
+
try {
|
|
921
|
+
chain = signatureChain(entry);
|
|
922
|
+
} catch (err) {
|
|
923
|
+
report.results.push({
|
|
924
|
+
index,
|
|
925
|
+
depth: 0,
|
|
926
|
+
path: `signatures[${index}]`,
|
|
927
|
+
algorithm: entry?.algorithm,
|
|
928
|
+
hashAlgorithm: entry?.hash_algorithm,
|
|
929
|
+
status: "failed",
|
|
930
|
+
valid: false,
|
|
931
|
+
trusted: false,
|
|
932
|
+
claimsTrustedKey: false,
|
|
933
|
+
keyIndex: undefined,
|
|
934
|
+
reason: err.message,
|
|
935
|
+
});
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
total += chain.length;
|
|
939
|
+
if (total > MAX_SIGNATURE_ENTRIES) {
|
|
940
|
+
report.reasons.push(
|
|
941
|
+
`The object holds more than ${MAX_SIGNATURE_ENTRIES} signature objects.`,
|
|
942
|
+
);
|
|
943
|
+
return report;
|
|
944
|
+
}
|
|
945
|
+
for (let depth = 0; depth < chain.length; depth++) {
|
|
946
|
+
const result = checkSignatureObject(
|
|
947
|
+
chain[depth],
|
|
948
|
+
serialize,
|
|
949
|
+
chainView(chain, depth),
|
|
950
|
+
trustedKeys,
|
|
951
|
+
{ allowEmbeddedKey },
|
|
952
|
+
);
|
|
953
|
+
if (result.keyIndex !== undefined) {
|
|
954
|
+
report.keysMatched[result.keyIndex] = true;
|
|
955
|
+
}
|
|
956
|
+
report.results.push({
|
|
957
|
+
index,
|
|
958
|
+
depth,
|
|
959
|
+
path: `signatures[${index}]${".signature".repeat(depth)}`,
|
|
960
|
+
...result,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const { results } = report;
|
|
965
|
+
if (trustedKeys.length) {
|
|
966
|
+
// A countersignature vouches for the signature it is attached to, so one
|
|
967
|
+
// that no trusted key verifies is a claim the reader cannot check. It
|
|
968
|
+
// fails the verdict like an entry that names a trusted key and does not
|
|
969
|
+
// verify; the caller passes the countersigner's key to accept it.
|
|
970
|
+
const failedClaim = results.some(
|
|
971
|
+
(result) =>
|
|
972
|
+
result.status !== "verified" &&
|
|
973
|
+
(result.claimsTrustedKey || result.depth > 0),
|
|
974
|
+
);
|
|
975
|
+
for (const result of results) {
|
|
976
|
+
if (result.depth > 0 && result.status !== "verified") {
|
|
977
|
+
report.reasons.push(
|
|
978
|
+
`The countersignature at ${result.path} does not verify with a trusted key; pass the countersigner's public key to accept it.`,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
report.valid = options.requireAll
|
|
983
|
+
? results.every((result) => result.status === "verified") &&
|
|
984
|
+
(!options.requireEveryKey || report.keysMatched.every(Boolean))
|
|
985
|
+
: results.some((result) => result.status === "verified") && !failedClaim;
|
|
986
|
+
report.trusted = report.valid;
|
|
987
|
+
} else {
|
|
988
|
+
report.valid = results.every((result) => result.valid);
|
|
989
|
+
}
|
|
990
|
+
return report;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function hasBomIdentity(bom) {
|
|
994
|
+
return (
|
|
995
|
+
(bom.specFormat === "CycloneDX" || bom.bomFormat === "CycloneDX") &&
|
|
996
|
+
typeof bom.specVersion === "string" &&
|
|
997
|
+
bom.specVersion !== ""
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function hasSignatureEntries(item) {
|
|
1002
|
+
return (
|
|
1003
|
+
isPlainObject(item) &&
|
|
1004
|
+
Array.isArray(item.signatures) &&
|
|
1005
|
+
item.signatures.length > 0
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function nestedTargets(bom) {
|
|
1010
|
+
const targets = [];
|
|
1011
|
+
const add = (path, item) => targets.push({ path, item });
|
|
1012
|
+
if (Array.isArray(bom.inventories)) {
|
|
1013
|
+
bom.inventories.forEach((inventory, index) => {
|
|
1014
|
+
add(`inventories[${index}]`, inventory);
|
|
1015
|
+
if (Array.isArray(inventory?.components)) {
|
|
1016
|
+
inventory.components.forEach((component, componentIndex) => {
|
|
1017
|
+
add(`inventories[${index}].components[${componentIndex}]`, component);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
for (const member of ["compositions", "annotations"]) {
|
|
1023
|
+
if (Array.isArray(bom[member])) {
|
|
1024
|
+
bom[member].forEach((item, index) => {
|
|
1025
|
+
add(`${member}[${index}]`, item);
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return targets;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Remove every JSS signature from a CycloneDX 2.0 BOM: the root and the
|
|
1034
|
+
* nested members {@link signJssBom} signs. Members are only touched where
|
|
1035
|
+
* they carry `signatures`. The input is not mutated.
|
|
1036
|
+
*
|
|
1037
|
+
* @param {object} bom20 - CycloneDX 2.0 document
|
|
1038
|
+
* @returns {{bom: object, removed: number}} The unsigned document and the number of signature entries removed
|
|
1039
|
+
*/
|
|
1040
|
+
export function removeJssSignatures(bom20) {
|
|
1041
|
+
let removed = 0;
|
|
1042
|
+
const strip = (item) => {
|
|
1043
|
+
if (!isPlainObject(item) || !Object.hasOwn(item, "signatures")) {
|
|
1044
|
+
return item;
|
|
1045
|
+
}
|
|
1046
|
+
const { signatures, ...rest } = item;
|
|
1047
|
+
removed += Array.isArray(signatures) ? signatures.length : 1;
|
|
1048
|
+
return rest;
|
|
1049
|
+
};
|
|
1050
|
+
const bom = strip(bom20);
|
|
1051
|
+
if (Array.isArray(bom.inventories)) {
|
|
1052
|
+
bom.inventories = bom.inventories.map((inventory) => {
|
|
1053
|
+
const unsigned = strip(inventory);
|
|
1054
|
+
return isPlainObject(unsigned) && Array.isArray(unsigned.components)
|
|
1055
|
+
? { ...unsigned, components: unsigned.components.map(strip) }
|
|
1056
|
+
: unsigned;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
for (const member of ["compositions", "annotations"]) {
|
|
1060
|
+
if (Array.isArray(bom[member])) {
|
|
1061
|
+
bom[member] = bom[member].map(strip);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return { bom, removed };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Sign a CycloneDX 2.0 BOM: the root document plus, with `deep`, the nested
|
|
1069
|
+
* members the draft schema gives a `signatures` member to (inventories, the
|
|
1070
|
+
* components inside an inventory, compositions and annotations). Nested
|
|
1071
|
+
* members are signed first and the root last, so the root signature covers
|
|
1072
|
+
* them.
|
|
1073
|
+
*
|
|
1074
|
+
* Existing root entries stay valid when only the root is signed (clause
|
|
1075
|
+
* 7.1.2). Signing nested members changes content that existing root entries
|
|
1076
|
+
* cover (clause 7.1.4 canonicalizes the whole object), so `deep` signing of
|
|
1077
|
+
* a BOM that already carries root signatures is refused unless
|
|
1078
|
+
* `allowInvalidatingSignatures` is set.
|
|
1079
|
+
*
|
|
1080
|
+
* @param {object} bom20 - Rendered CycloneDX 2.0 document
|
|
1081
|
+
* @param {object} options - Passed to {@link signJss}
|
|
1082
|
+
* @param {boolean} [options.deep] - Also sign nested members (default true)
|
|
1083
|
+
* @param {boolean} [options.allowInvalidatingSignatures] - Sign nested members even though existing root signatures will no longer verify
|
|
1084
|
+
* @returns {object} The signed document
|
|
1085
|
+
*/
|
|
1086
|
+
export function signJssBom(bom20, options = {}) {
|
|
1087
|
+
const {
|
|
1088
|
+
deep = true,
|
|
1089
|
+
allowInvalidatingSignatures = false,
|
|
1090
|
+
...signOptions
|
|
1091
|
+
} = options;
|
|
1092
|
+
if (!isPlainObject(bom20)) {
|
|
1093
|
+
throw new Error("The BOM must be a JSON object.");
|
|
1094
|
+
}
|
|
1095
|
+
if (!deep) {
|
|
1096
|
+
return signJss(bom20, signOptions);
|
|
1097
|
+
}
|
|
1098
|
+
if (hasSignatureEntries(bom20) && !allowInvalidatingSignatures) {
|
|
1099
|
+
throw new Error(
|
|
1100
|
+
"Signing nested members would change content that the existing root JSS signature(s) cover, so they would no longer verify. Sign the root only, or remove the existing signatures first.",
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
const signItem = (item) =>
|
|
1104
|
+
isPlainObject(item) ? signJss(item, signOptions) : item;
|
|
1105
|
+
let signed = bom20;
|
|
1106
|
+
if (Array.isArray(signed.inventories)) {
|
|
1107
|
+
signed = {
|
|
1108
|
+
...signed,
|
|
1109
|
+
inventories: signed.inventories.map((inventory) => {
|
|
1110
|
+
if (!isPlainObject(inventory)) {
|
|
1111
|
+
return inventory;
|
|
1112
|
+
}
|
|
1113
|
+
// Inside out: components first, then the inventory (which covers
|
|
1114
|
+
// the signed components), the root last.
|
|
1115
|
+
return signItem(
|
|
1116
|
+
Array.isArray(inventory.components)
|
|
1117
|
+
? { ...inventory, components: inventory.components.map(signItem) }
|
|
1118
|
+
: inventory,
|
|
1119
|
+
);
|
|
1120
|
+
}),
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
for (const member of NESTED_SIGNATURE_TARGETS.slice(1)) {
|
|
1124
|
+
if (Array.isArray(signed[member])) {
|
|
1125
|
+
signed = { ...signed, [member]: signed[member].map(signItem) };
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return signJss(signed, signOptions);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Verify the JSS signatures of a CycloneDX 2.0 BOM, root and nested, with
|
|
1133
|
+
* the same policy {@link verifyBomSignatures} applies to JSF:
|
|
1134
|
+
*
|
|
1135
|
+
* - the root must carry `specFormat` (or `bomFormat`) "CycloneDX" and
|
|
1136
|
+
* `specVersion`, and only members CycloneDX defines at the root;
|
|
1137
|
+
* - the root signatures verify as {@link verifyJss} defines; with
|
|
1138
|
+
* `requireAll`, every root signature object verifies with a trusted key
|
|
1139
|
+
* and every trusted key verifies one of them;
|
|
1140
|
+
* - with `deep` (default), every signed inventory, inventory component,
|
|
1141
|
+
* composition and annotation verifies the same way (without the
|
|
1142
|
+
* every-key rule) and carries none of `bomFormat`, `specFormat`,
|
|
1143
|
+
* `specVersion`;
|
|
1144
|
+
* - a BOM without root signatures fails unless `allowNestedOnly` is set;
|
|
1145
|
+
* then `deep` must be on and every nested member must be signed.
|
|
1146
|
+
*
|
|
1147
|
+
* @param {object} bom20 - Signed CycloneDX 2.0 document
|
|
1148
|
+
* @param {object} options - Trust options passed to {@link verifyJss}
|
|
1149
|
+
* @param {boolean} [options.deep] - Also verify nested signatures (default true)
|
|
1150
|
+
* @param {boolean} [options.requireAll] - Every signature object must verify with a trusted key
|
|
1151
|
+
* @param {boolean} [options.allowNestedOnly] - Accept a BOM without root signatures when every nested member is signed
|
|
1152
|
+
* @returns {{valid: boolean, trusted: boolean, errors: string[], root: (object|null), results: Array, nested: Array<{path: string, valid: boolean, trusted: boolean, reasons: string[], results: Array}>}}
|
|
1153
|
+
* @throws {Error} When neither a trusted key nor allowEmbeddedKey is given
|
|
1154
|
+
*/
|
|
1155
|
+
export function verifyJssBom(bom20, options = {}) {
|
|
1156
|
+
const deep = options.deep !== false;
|
|
1157
|
+
const allowNestedOnly = Boolean(options.allowNestedOnly);
|
|
1158
|
+
const requireAll = Boolean(options.requireAll);
|
|
1159
|
+
const trustOptions = {
|
|
1160
|
+
publicKey: options.publicKey,
|
|
1161
|
+
publicKeys: options.publicKeys,
|
|
1162
|
+
allowEmbeddedKey: Boolean(options.allowEmbeddedKey),
|
|
1163
|
+
requireAll,
|
|
1164
|
+
};
|
|
1165
|
+
requireTrust(loadTrustedKeys(trustOptions), trustOptions.allowEmbeddedKey);
|
|
1166
|
+
const report = {
|
|
1167
|
+
valid: false,
|
|
1168
|
+
trusted: false,
|
|
1169
|
+
errors: [],
|
|
1170
|
+
root: null,
|
|
1171
|
+
results: [],
|
|
1172
|
+
nested: [],
|
|
1173
|
+
};
|
|
1174
|
+
if (!isPlainObject(bom20)) {
|
|
1175
|
+
report.errors.push("The BOM must be a JSON object.");
|
|
1176
|
+
return report;
|
|
1177
|
+
}
|
|
1178
|
+
if (!hasBomIdentity(bom20)) {
|
|
1179
|
+
report.errors.push(
|
|
1180
|
+
'The signed object is not a CycloneDX BOM: its root needs specFormat "CycloneDX" and a specVersion.',
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const unexpected = Object.keys(bom20).filter(
|
|
1184
|
+
(name) => !BOM_ROOT_MEMBER_SET.has(name),
|
|
1185
|
+
);
|
|
1186
|
+
if (unexpected.length) {
|
|
1187
|
+
report.errors.push(
|
|
1188
|
+
`The BOM has members that CycloneDX does not define at the root of a BOM: ${unexpected.slice(0, 10).map(printable).join(", ")}.`,
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const nestedOnly = !hasSignatureEntries(bom20);
|
|
1192
|
+
if (!nestedOnly) {
|
|
1193
|
+
report.root = verifyJss(bom20, {
|
|
1194
|
+
...trustOptions,
|
|
1195
|
+
requireEveryKey: requireAll,
|
|
1196
|
+
});
|
|
1197
|
+
report.results = report.root.results;
|
|
1198
|
+
if (requireAll) {
|
|
1199
|
+
const unmatched = report.root.keysMatched.flatMap((matched, index) =>
|
|
1200
|
+
matched ? [] : [`#${index + 1}`],
|
|
1201
|
+
);
|
|
1202
|
+
if (unmatched.length) {
|
|
1203
|
+
report.errors.push(
|
|
1204
|
+
`No root signature verifies with key ${unmatched.join(", ")}, so a signature expected from that key is missing.`,
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
} else if (
|
|
1209
|
+
bom20.signatures !== undefined &&
|
|
1210
|
+
!Array.isArray(bom20.signatures)
|
|
1211
|
+
) {
|
|
1212
|
+
report.errors.push("The root signatures member must be a list.");
|
|
1213
|
+
} else if (!allowNestedOnly) {
|
|
1214
|
+
report.errors.push(
|
|
1215
|
+
"The BOM has no root signature, so no signature covers the BOM as a whole.",
|
|
1216
|
+
);
|
|
1217
|
+
} else if (!deep) {
|
|
1218
|
+
report.errors.push(
|
|
1219
|
+
"The BOM has no root signature, and nested signatures were not checked.",
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
if (deep) {
|
|
1223
|
+
const unsigned = [];
|
|
1224
|
+
for (const { path, item } of nestedTargets(bom20)) {
|
|
1225
|
+
if (!hasSignatureEntries(item)) {
|
|
1226
|
+
unsigned.push(path);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const identity = BOM_IDENTITY_MEMBERS.filter((name) =>
|
|
1230
|
+
Object.hasOwn(item, name),
|
|
1231
|
+
);
|
|
1232
|
+
if (identity.length) {
|
|
1233
|
+
report.errors.push(
|
|
1234
|
+
`${path} carries ${identity.join(", ")}, which only the root of a BOM may hold.`,
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
report.nested.push({ path, ...verifyJss(item, trustOptions) });
|
|
1238
|
+
}
|
|
1239
|
+
if (nestedOnly && allowNestedOnly) {
|
|
1240
|
+
if (!report.nested.length) {
|
|
1241
|
+
report.errors.push("The BOM has no signatures.");
|
|
1242
|
+
}
|
|
1243
|
+
if (unsigned.length) {
|
|
1244
|
+
report.errors.push(
|
|
1245
|
+
`${unsigned.length} nested members are not signed, starting with ${unsigned[0]}.`,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
report.valid =
|
|
1251
|
+
!report.errors.length &&
|
|
1252
|
+
(report.root ? report.root.valid : allowNestedOnly && deep) &&
|
|
1253
|
+
report.nested.every((item) => item.valid);
|
|
1254
|
+
report.trusted =
|
|
1255
|
+
report.valid &&
|
|
1256
|
+
(report.root ? report.root.trusted : true) &&
|
|
1257
|
+
report.nested.every((item) => item.trusted);
|
|
1258
|
+
return report;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Generate a JSS key pair for an algorithm (used by --generate-key-and-sign).
|
|
1263
|
+
*
|
|
1264
|
+
* @param {string} algorithm - JSS algorithm name
|
|
1265
|
+
* @returns {{privateKey: KeyObject, publicKey: KeyObject}}
|
|
1266
|
+
*/
|
|
1267
|
+
export function generateJssKeyPair(algorithm) {
|
|
1268
|
+
const spec = algorithmSpec(algorithm);
|
|
1269
|
+
if (spec.family === "rsa" || spec.family === "rsa-pss") {
|
|
1270
|
+
return generateKeyPairSync("rsa", { modulusLength: 3072 });
|
|
1271
|
+
}
|
|
1272
|
+
if (spec.family === "ec") {
|
|
1273
|
+
return generateKeyPairSync("ec", { namedCurve: spec.curve });
|
|
1274
|
+
}
|
|
1275
|
+
return generateKeyPairSync(spec.keyTypes[0]);
|
|
1276
|
+
}
|