@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.
Files changed (264) hide show
  1. package/README.md +15 -8
  2. package/bin/audit.js +7 -4
  3. package/bin/cdxgen.js +256 -37
  4. package/bin/convert.js +146 -22
  5. package/bin/evinse.js +25 -3
  6. package/bin/repl.js +47 -3
  7. package/bin/sign.js +211 -34
  8. package/bin/tracebom.js +18 -6
  9. package/bin/validate.js +18 -3
  10. package/bin/verify.js +113 -5
  11. package/data/behavior-taxonomy.schema.json +1491 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
  13. package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
  14. package/data/pqc-migration.json +239 -0
  15. package/data/remediations.json +52 -0
  16. package/data/rules/cbom-security.yaml +4 -3
  17. package/data/rules/package-integrity.yaml +28 -0
  18. package/lib/audit/index.js +25 -3
  19. package/lib/cli/bomAssembly.js +80 -1
  20. package/lib/cli/cliOptions.js +121 -17
  21. package/lib/cli/index.js +609 -898
  22. package/lib/cli/jsBom.js +3 -0
  23. package/lib/cli/jvmBom.js +365 -40
  24. package/lib/cli/managedBom.js +19 -3
  25. package/lib/cli/nativeBom.js +10 -0
  26. package/lib/core/env.js +26 -0
  27. package/lib/core/fs.js +126 -3
  28. package/lib/core/globPatterns.js +218 -0
  29. package/lib/core/processTree.js +99 -12
  30. package/lib/core/projectConfig.js +332 -25
  31. package/lib/core/toolRegistry.js +15 -1
  32. package/lib/ecosystems/denoutils.js +2 -2
  33. package/lib/ecosystems/golem.js +3 -8
  34. package/lib/ecosystems/gradleutils.js +1082 -6
  35. package/lib/ecosystems/jsEvidence.js +90 -38
  36. package/lib/ecosystems/kosi.js +3 -5
  37. package/lib/ecosystems/npmVersionResolution.js +376 -0
  38. package/lib/ecosystems/parsers-python.js +89 -28
  39. package/lib/ecosystems/parsers-rust.js +117 -15
  40. package/lib/ecosystems/remote/huggingface.js +22 -0
  41. package/lib/ecosystems/rusi.js +2 -4
  42. package/lib/ecosystems/scalasem.js +2 -1
  43. package/lib/evinser/evinser.js +196 -34
  44. package/lib/evinser/scalasem.js +5 -1
  45. package/lib/helpers/agenticToolutils.js +112 -1
  46. package/lib/helpers/bomSigner.js +43 -9
  47. package/lib/helpers/jssSigner.js +1276 -0
  48. package/lib/inventory/agentBlueprint.js +193 -0
  49. package/lib/inventory/agentToolNames.js +145 -0
  50. package/lib/inventory/aiCollector.js +153 -7
  51. package/lib/inventory/aiInventory.js +44 -8
  52. package/lib/inventory/aiProviders.js +324 -0
  53. package/lib/inventory/bomUtils.js +8 -1
  54. package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
  55. package/lib/inventory/citations.js +31 -2
  56. package/lib/inventory/communityAiConfigParser.js +978 -80
  57. package/lib/inventory/cyclonedx20.js +152 -0
  58. package/lib/inventory/cyclonedx20Common.js +286 -0
  59. package/lib/inventory/cyclonedx20Derived.js +159 -0
  60. package/lib/inventory/cyclonedx20Parties.js +552 -0
  61. package/lib/inventory/cyclonedx20Read.js +768 -0
  62. package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
  63. package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
  64. package/lib/inventory/cyclonedx20ReadContext.js +380 -0
  65. package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
  66. package/lib/inventory/cyclonedx20ReadSections.js +599 -0
  67. package/lib/inventory/cyclonedx20Render.js +766 -0
  68. package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
  69. package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
  70. package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
  71. package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
  72. package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
  73. package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
  74. package/lib/inventory/cyclonedx20RenderSections.js +499 -0
  75. package/lib/inventory/cyclonedx20Tables.js +594 -0
  76. package/lib/inventory/deps.js +106 -13
  77. package/lib/inventory/dosai.js +236 -1
  78. package/lib/inventory/dosaiParsers.js +3 -6
  79. package/lib/inventory/dotnetRuntime.js +164 -14
  80. package/lib/inventory/ecosystemFormulationParsers.js +979 -0
  81. package/lib/inventory/envcontext.js +132 -38
  82. package/lib/inventory/evidenceUtils.js +2 -1
  83. package/lib/inventory/formulationParsers.js +95 -13
  84. package/lib/inventory/hbom.js +45 -6
  85. package/lib/inventory/hbomLoader.js +166 -0
  86. package/lib/inventory/mcpConfigParser.js +5 -0
  87. package/lib/inventory/occurrenceKey.js +90 -0
  88. package/lib/inventory/pqcMigration.js +426 -0
  89. package/lib/inventory/rustFormulationParser.js +21 -10
  90. package/lib/inventory/source.js +65 -6
  91. package/lib/parsers/huggingfaceManifest.js +9 -1
  92. package/lib/parsers/huggingfaceUtils.js +18 -0
  93. package/lib/parsers/kiroPermissions.js +166 -0
  94. package/lib/server/server.js +36 -34
  95. package/lib/stages/postgen/postgen.js +41 -1
  96. package/lib/stages/postgen/specVersionCompat.js +76 -0
  97. package/lib/validator/bomValidator.js +8 -1
  98. package/lib/validator/complianceEngine.js +30 -3
  99. package/lib/validator/complianceRules.js +1416 -4
  100. package/lib/validator/generated/validate-2.0.mjs +57554 -13851
  101. package/lib/validator/index.js +48 -7
  102. package/lib/validator/reporters/json.js +6 -0
  103. package/package.json +9 -9
  104. package/types/bin/repl.d.ts.map +1 -1
  105. package/types/lib/audit/index.d.ts +6 -2
  106. package/types/lib/audit/index.d.ts.map +1 -1
  107. package/types/lib/cli/bomAssembly.d.ts +16 -0
  108. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  109. package/types/lib/cli/cliOptions.d.ts +6 -0
  110. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  111. package/types/lib/cli/index.d.ts.map +1 -1
  112. package/types/lib/cli/jsBom.d.ts +2 -1
  113. package/types/lib/cli/jsBom.d.ts.map +1 -1
  114. package/types/lib/cli/jvmBom.d.ts +15 -0
  115. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  116. package/types/lib/cli/managedBom.d.ts.map +1 -1
  117. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  118. package/types/lib/core/boundedFile.d.ts +30 -0
  119. package/types/lib/core/boundedFile.d.ts.map +1 -1
  120. package/types/lib/core/env.d.ts +10 -0
  121. package/types/lib/core/env.d.ts.map +1 -1
  122. package/types/lib/core/fs.d.ts +4 -0
  123. package/types/lib/core/fs.d.ts.map +1 -1
  124. package/types/lib/core/globPatterns.d.ts +70 -0
  125. package/types/lib/core/globPatterns.d.ts.map +1 -0
  126. package/types/lib/core/paths.d.ts +9 -0
  127. package/types/lib/core/paths.d.ts.map +1 -1
  128. package/types/lib/core/processTree.d.ts +16 -5
  129. package/types/lib/core/processTree.d.ts.map +1 -1
  130. package/types/lib/core/projectConfig.d.ts +47 -8
  131. package/types/lib/core/projectConfig.d.ts.map +1 -1
  132. package/types/lib/core/toolRegistry.d.ts.map +1 -1
  133. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  134. package/types/lib/ecosystems/gradleutils.d.ts +248 -8
  135. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  136. package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
  137. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  138. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  139. package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
  140. package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
  141. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  142. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  143. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  144. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  145. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  146. package/types/lib/ecosystems/scalasem.d.ts +58 -16
  147. package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
  148. package/types/lib/evinser/evinser.d.ts +61 -9
  149. package/types/lib/evinser/evinser.d.ts.map +1 -1
  150. package/types/lib/evinser/scalasem.d.ts.map +1 -1
  151. package/types/lib/helpers/agenticToolutils.d.ts +53 -19
  152. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
  153. package/types/lib/helpers/bomSigner.d.ts +20 -3
  154. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  155. package/types/lib/helpers/jssSigner.d.ts +288 -0
  156. package/types/lib/helpers/jssSigner.d.ts.map +1 -0
  157. package/types/lib/helpers/vsixutils.d.ts +303 -19
  158. package/types/lib/helpers/vsixutils.d.ts.map +1 -1
  159. package/types/lib/inventory/agentBlueprint.d.ts +70 -0
  160. package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
  161. package/types/lib/inventory/agentToolNames.d.ts +51 -0
  162. package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
  163. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  164. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  165. package/types/lib/inventory/aiProviders.d.ts +85 -0
  166. package/types/lib/inventory/aiProviders.d.ts.map +1 -0
  167. package/types/lib/inventory/bomUtils.d.ts.map +1 -1
  168. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
  169. package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
  170. package/types/lib/inventory/citations.d.ts +5 -0
  171. package/types/lib/inventory/citations.d.ts.map +1 -1
  172. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  173. package/types/lib/inventory/cyclonedx20.d.ts +99 -0
  174. package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
  175. package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
  176. package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
  177. package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
  178. package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
  179. package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
  180. package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
  181. package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
  182. package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
  183. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
  184. package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
  185. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
  186. package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
  187. package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
  188. package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
  189. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
  190. package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
  191. package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
  192. package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
  193. package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
  194. package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
  195. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
  196. package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
  197. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
  198. package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
  199. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
  200. package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
  201. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
  202. package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
  203. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
  204. package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
  205. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
  206. package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
  207. package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
  208. package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
  209. package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
  210. package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
  211. package/types/lib/inventory/deps.d.ts +35 -2
  212. package/types/lib/inventory/deps.d.ts.map +1 -1
  213. package/types/lib/inventory/dosai.d.ts +19 -0
  214. package/types/lib/inventory/dosai.d.ts.map +1 -1
  215. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  216. package/types/lib/inventory/dotnetRuntime.d.ts +51 -4
  217. package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -1
  218. package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
  219. package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
  220. package/types/lib/inventory/envcontext.d.ts +22 -6
  221. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  222. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
  224. package/types/lib/inventory/hbom.d.ts +11 -0
  225. package/types/lib/inventory/hbom.d.ts.map +1 -1
  226. package/types/lib/inventory/hbomLoader.d.ts +54 -0
  227. package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
  228. package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
  229. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
  230. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  231. package/types/lib/inventory/occurrenceKey.d.ts +52 -0
  232. package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
  233. package/types/lib/inventory/pqcMigration.d.ts +84 -0
  234. package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
  235. package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
  236. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  237. package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
  238. package/types/lib/inventory/scalaDedupe.d.ts +43 -0
  239. package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
  240. package/types/lib/inventory/source.d.ts +15 -0
  241. package/types/lib/inventory/source.d.ts.map +1 -1
  242. package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
  243. package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
  244. package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
  245. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  246. package/types/lib/parsers/kiroPermissions.d.ts +62 -0
  247. package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
  248. package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
  249. package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
  250. package/types/lib/server/server.d.ts.map +1 -1
  251. package/types/lib/stages/postgen/postgen.d.ts +7 -0
  252. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  253. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  254. package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
  255. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  256. package/types/lib/validator/bomValidator.d.ts +5 -1
  257. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  258. package/types/lib/validator/complianceEngine.d.ts +4 -4
  259. package/types/lib/validator/complianceEngine.d.ts.map +1 -1
  260. package/types/lib/validator/complianceRules.d.ts +14 -2
  261. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  262. package/types/lib/validator/index.d.ts +10 -0
  263. package/types/lib/validator/index.d.ts.map +1 -1
  264. 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
+ }