@cdxgen/cdxgen 13.2.0 → 13.3.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 (158) hide show
  1. package/README.md +28 -12
  2. package/bin/cdxgen.js +227 -119
  3. package/bin/evinse.js +5 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/remediations.json +24 -0
  9. package/lib/cli/bomAssembly.js +105 -15
  10. package/lib/cli/cliOptions.js +93 -1
  11. package/lib/cli/index.js +43 -4
  12. package/lib/cli/jsBom.js +49 -24
  13. package/lib/cli/jvmBom.js +46 -12
  14. package/lib/cli/managedBom.js +112 -41
  15. package/lib/cli/nativeBom.js +125 -14
  16. package/lib/core/env.js +5 -6
  17. package/lib/core/fs.js +174 -6
  18. package/lib/core/metadataProperties.js +97 -0
  19. package/lib/core/projectConfig.js +31 -4
  20. package/lib/ecosystems/bunutils.js +269 -101
  21. package/lib/ecosystems/caxa.js +434 -2
  22. package/lib/ecosystems/cmakeResolver.js +66 -0
  23. package/lib/ecosystems/cppEvidence.js +64 -31
  24. package/lib/ecosystems/ecosystems.js +19 -8
  25. package/lib/ecosystems/gradleutils.js +42 -7
  26. package/lib/ecosystems/jsEvidence.js +19 -20
  27. package/lib/ecosystems/kosi.js +10 -3
  28. package/lib/ecosystems/npmutils.js +110 -9
  29. package/lib/ecosystems/parsers-dotnet.js +51 -10
  30. package/lib/ecosystems/parsers-go.js +47 -9
  31. package/lib/ecosystems/parsers-js.js +171 -50
  32. package/lib/ecosystems/parsers-misc.js +747 -161
  33. package/lib/ecosystems/parsers-python.js +26 -14
  34. package/lib/ecosystems/parsers-rust.js +24 -18
  35. package/lib/ecosystems/rubyutils.js +1426 -166
  36. package/lib/ecosystems/rusi.js +32 -1
  37. package/lib/evinser/evinser.js +436 -128
  38. package/lib/evinser/swiftsem.js +1914 -302
  39. package/lib/helpers/bomSigner.js +652 -221
  40. package/lib/inventory/atomUtils.js +568 -31
  41. package/lib/inventory/cdxrs.js +103 -12
  42. package/lib/inventory/depsUtils.js +183 -11
  43. package/lib/inventory/display.js +4 -5
  44. package/lib/inventory/dosai.js +550 -13
  45. package/lib/inventory/dynamic.js +39 -1
  46. package/lib/inventory/evidenceUtils.js +14 -6
  47. package/lib/inventory/fetchBatch.js +116 -17
  48. package/lib/inventory/protobom.js +8 -1
  49. package/lib/inventory/purl.js +152 -3
  50. package/lib/inventory/sbomDocument.js +2 -70
  51. package/lib/inventory/source.js +37 -19
  52. package/lib/inventory/traceRunner.js +210 -21
  53. package/lib/managers/binary.js +115 -24
  54. package/lib/managers/docker.js +440 -20
  55. package/lib/managers/oci.js +39 -1
  56. package/lib/parsers/cmakeLists.js +452 -0
  57. package/lib/parsers/largeJson.js +454 -0
  58. package/lib/parsers/npmrc.js +19 -1
  59. package/lib/stages/postgen/postgen.js +68 -15
  60. package/lib/validator/bomValidator.js +6 -16
  61. package/lib/validator/index.js +48 -10
  62. package/lib/validator/reporters/console.js +3 -0
  63. package/lib/validator/reporters/json.js +1 -0
  64. package/lib/validator/reporters/sarif.js +7 -0
  65. package/package.json +20 -20
  66. package/types/lib/cli/bomAssembly.d.ts +2 -1
  67. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  68. package/types/lib/cli/cliOptions.d.ts +33 -1
  69. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  70. package/types/lib/cli/index.d.ts.map +1 -1
  71. package/types/lib/cli/jsBom.d.ts.map +1 -1
  72. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  73. package/types/lib/cli/managedBom.d.ts.map +1 -1
  74. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  75. package/types/lib/core/env.d.ts.map +1 -1
  76. package/types/lib/core/fs.d.ts +55 -0
  77. package/types/lib/core/fs.d.ts.map +1 -1
  78. package/types/lib/core/metadataProperties.d.ts +39 -0
  79. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  80. package/types/lib/core/projectConfig.d.ts +6 -2
  81. package/types/lib/core/projectConfig.d.ts.map +1 -1
  82. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  83. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  84. package/types/lib/ecosystems/caxa.d.ts +14 -0
  85. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  86. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  87. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  88. package/types/lib/ecosystems/cppEvidence.d.ts +1 -1
  89. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  90. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  91. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  92. package/types/lib/ecosystems/gradleutils.d.ts +20 -4
  93. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  94. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  95. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  96. package/types/lib/ecosystems/kosi.d.ts +13 -0
  97. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  98. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  99. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  100. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  101. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  102. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  103. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  104. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  105. package/types/lib/ecosystems/parsers-misc.d.ts +67 -3
  106. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  107. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  108. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  109. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  110. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  111. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  112. package/types/lib/ecosystems/rusi.d.ts +13 -0
  113. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  114. package/types/lib/evinser/evinser.d.ts +59 -9
  115. package/types/lib/evinser/evinser.d.ts.map +1 -1
  116. package/types/lib/evinser/swiftsem.d.ts +280 -15
  117. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  118. package/types/lib/helpers/bomSigner.d.ts +106 -13
  119. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  120. package/types/lib/inventory/atomUtils.d.ts +145 -3
  121. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  122. package/types/lib/inventory/cdxrs.d.ts +8 -3
  123. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  124. package/types/lib/inventory/depsUtils.d.ts +15 -0
  125. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  126. package/types/lib/inventory/display.d.ts.map +1 -1
  127. package/types/lib/inventory/dosai.d.ts +64 -1
  128. package/types/lib/inventory/dosai.d.ts.map +1 -1
  129. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  130. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  131. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  132. package/types/lib/inventory/plugins.d.ts.map +1 -1
  133. package/types/lib/inventory/protobom.d.ts.map +1 -1
  134. package/types/lib/inventory/purl.d.ts +71 -2
  135. package/types/lib/inventory/purl.d.ts.map +1 -1
  136. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  137. package/types/lib/inventory/source.d.ts.map +1 -1
  138. package/types/lib/inventory/traceRunner.d.ts +47 -1
  139. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  140. package/types/lib/managers/binary.d.ts +30 -1
  141. package/types/lib/managers/binary.d.ts.map +1 -1
  142. package/types/lib/managers/docker.d.ts +29 -0
  143. package/types/lib/managers/docker.d.ts.map +1 -1
  144. package/types/lib/managers/oci.d.ts +14 -0
  145. package/types/lib/managers/oci.d.ts.map +1 -1
  146. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  147. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  148. package/types/lib/parsers/largeJson.d.ts +52 -0
  149. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  150. package/types/lib/parsers/npmrc.d.ts +5 -0
  151. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  152. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  153. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  154. package/types/lib/validator/index.d.ts +10 -1
  155. package/types/lib/validator/index.d.ts.map +1 -1
  156. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  157. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  158. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -1,137 +1,540 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
3
  /**
4
- * Lightweight, deterministic JSON Canonicalizer (similar to RFC 8785).
5
- * Required by JSF to ensure the signature payload remains identical across systems.
4
+ * JSF algorithm identifiers and the key material each one requires.
5
+ *
6
+ * The algorithm named in a signature block is chosen by whoever produced the
7
+ * document, so it is only honoured when the key supplied by the signer or the
8
+ * verifier is exactly the type listed here.
9
+ */
10
+ const JSF_ALGORITHMS = new Map([
11
+ ["RS256", { keyType: "rsa", hash: "sha256" }],
12
+ ["RS384", { keyType: "rsa", hash: "sha384" }],
13
+ ["RS512", { keyType: "rsa", hash: "sha512" }],
14
+ ["PS256", { keyType: "rsa-pss", hash: "sha256" }],
15
+ ["PS384", { keyType: "rsa-pss", hash: "sha384" }],
16
+ ["PS512", { keyType: "rsa-pss", hash: "sha512" }],
17
+ ["ES256", { keyType: "ec", hash: "sha256", curve: "prime256v1" }],
18
+ ["ES384", { keyType: "ec", hash: "sha384", curve: "secp384r1" }],
19
+ ["ES512", { keyType: "ec", hash: "sha512", curve: "secp521r1" }],
20
+ ["Ed25519", { keyType: "ed25519", hash: null }],
21
+ ["Ed448", { keyType: "ed448", hash: null }],
22
+ ["HS256", { keyType: "secret", hash: "sha256" }],
23
+ ["HS384", { keyType: "secret", hash: "sha384" }],
24
+ ["HS512", { keyType: "secret", hash: "sha512" }],
25
+ ]);
26
+
27
+ const SIGNATURE_MODES = ["replace", "signers", "chain"];
28
+
29
+ const NESTED_TARGETS = ["components", "services", "annotations"];
30
+
31
+ // DER layouts tried when checking whether raw bytes are really a key.
32
+ const DER_KEY_LAYOUTS = [
33
+ [crypto.createPublicKey, "spki"],
34
+ [crypto.createPublicKey, "pkcs1"],
35
+ [crypto.createPrivateKey, "pkcs8"],
36
+ [crypto.createPrivateKey, "pkcs1"],
37
+ [crypto.createPrivateKey, "sec1"],
38
+ ];
39
+
40
+ /**
41
+ * Renders a value taken from a document for a message. JSON can make any field
42
+ * an object or array, and interpolating those directly can throw.
43
+ *
44
+ * @param {any} value - Value to render
45
+ * @returns {string} - Printable text
46
+ */
47
+ export function displayValue(value) {
48
+ if (typeof value === "string") {
49
+ return value;
50
+ }
51
+ try {
52
+ return JSON.stringify(value) ?? String(value);
53
+ } catch {
54
+ return `<${typeof value}>`;
55
+ }
56
+ }
57
+
58
+ function isPlainObject(value) {
59
+ return value !== null && typeof value === "object" && !Array.isArray(value);
60
+ }
61
+
62
+ /**
63
+ * Deterministic JSON canonicalizer following RFC 8785 (JCS), which JSF uses to
64
+ * produce the signed data. Values are serialized the way JSON.stringify would
65
+ * see them, so an in-memory object and its parsed JSON form canonicalize
66
+ * identically.
6
67
  *
7
68
  * @param {any} value - The JSON object/value to canonicalize
8
- * @returns {string} - Canonicalized JSON string
69
+ * @returns {string|undefined} - Canonicalized JSON string, or undefined for values JSON omits
9
70
  */
10
71
  function canonicalize(value) {
72
+ if (typeof value?.toJSON === "function") {
73
+ value = value.toJSON();
74
+ }
11
75
  if (value === null || typeof value !== "object") {
12
76
  return JSON.stringify(value);
13
77
  }
14
78
  if (Array.isArray(value)) {
15
- return `[${value.map(canonicalize).join(",")}]`;
79
+ return `[${value.map((item) => canonicalize(item) ?? "null").join(",")}]`;
80
+ }
81
+ const members = [];
82
+ for (const key of Object.keys(value).sort()) {
83
+ const member = canonicalize(value[key]);
84
+ if (member !== undefined) {
85
+ members.push(`${JSON.stringify(key)}:${member}`);
86
+ }
87
+ }
88
+ return `{${members.join(",")}}`;
89
+ }
90
+
91
+ function getAlgorithm(alg) {
92
+ const spec = typeof alg === "string" ? JSF_ALGORITHMS.get(alg) : undefined;
93
+ if (!spec) {
94
+ const name =
95
+ typeof alg === "string" ? alg : `<${alg === null ? "null" : typeof alg}>`;
96
+ throw new Error(
97
+ `Unsupported JSF algorithm: ${name}. Supported algorithms: ${[...JSF_ALGORITHMS.keys()].join(", ")}.`,
98
+ );
99
+ }
100
+ return spec;
101
+ }
102
+
103
+ function describeRequirement(spec) {
104
+ if (spec.keyType === "secret") {
105
+ return "a shared secret";
106
+ }
107
+ if (spec.keyType === "rsa-pss") {
108
+ return "an rsa or rsa-pss key";
16
109
  }
17
- const keys = Object.keys(value).sort();
18
- let str = "{";
19
- for (let i = 0; i < keys.length; i++) {
20
- if (i > 0) str += ",";
21
- str += `${JSON.stringify(keys[i])}:${canonicalize(value[keys[i]])}`;
110
+ if (spec.curve) {
111
+ return `an ec (${spec.curve}) key`;
22
112
  }
23
- str += "}";
24
- return str;
113
+ return `an ${spec.keyType} key`;
114
+ }
115
+
116
+ function describeKey(key) {
117
+ if (key.type === "secret") {
118
+ return "a shared secret";
119
+ }
120
+ const curve = key.asymmetricKeyDetails?.namedCurve;
121
+ return curve
122
+ ? `${key.asymmetricKeyType} (${curve})`
123
+ : String(key.asymmetricKeyType);
124
+ }
125
+
126
+ function assertKeyMatchesAlgorithm(key, alg, spec) {
127
+ let matches;
128
+ if (spec.keyType === "secret" || key.type === "secret") {
129
+ matches = spec.keyType === "secret" && key.type === "secret";
130
+ } else if (spec.keyType === "rsa-pss") {
131
+ matches = ["rsa", "rsa-pss"].includes(key.asymmetricKeyType);
132
+ } else if (spec.keyType === "ec") {
133
+ matches =
134
+ key.asymmetricKeyType === "ec" &&
135
+ key.asymmetricKeyDetails?.namedCurve === spec.curve;
136
+ } else {
137
+ matches = key.asymmetricKeyType === spec.keyType;
138
+ }
139
+ if (!matches) {
140
+ throw new Error(
141
+ `Algorithm ${alg} requires ${describeRequirement(spec)}, but the key is ${describeKey(key)}.`,
142
+ );
143
+ }
144
+ }
145
+
146
+ function parsesAsKey(create, input) {
147
+ try {
148
+ create(input);
149
+ return true;
150
+ } catch (err) {
151
+ return err?.code === "ERR_MISSING_PASSPHRASE";
152
+ }
153
+ }
154
+
155
+ function isJwkText(material) {
156
+ try {
157
+ const parsed = JSON.parse(material.toString("utf8"));
158
+ return isPlainObject(parsed) && typeof parsed.kty === "string";
159
+ } catch {
160
+ return false;
161
+ }
162
+ }
163
+
164
+ function looksLikeAsymmetricKey(material) {
165
+ if (typeof material === "string" || Buffer.isBuffer(material)) {
166
+ if (material.includes("-----BEGIN ") || isJwkText(material)) {
167
+ return true;
168
+ }
169
+ }
170
+ if (
171
+ parsesAsKey(crypto.createPrivateKey, material) ||
172
+ parsesAsKey(crypto.createPublicKey, material)
173
+ ) {
174
+ return true;
175
+ }
176
+ if (Buffer.isBuffer(material)) {
177
+ return DER_KEY_LAYOUTS.some(([create, type]) =>
178
+ parsesAsKey(create, { key: material, format: "der", type }),
179
+ );
180
+ }
181
+ return false;
25
182
  }
26
183
 
27
184
  /**
28
- * Creates a JSF-compliant signature block.
185
+ * Wraps shared-secret material for HMAC (HS256, HS384, HS512) signing or
186
+ * verification.
29
187
  *
30
- * @param {Object} payload - The object to sign (e.g., BOM, component)
31
- * @param {string|Buffer|crypto.KeyObject} privateKey - The signing key
32
- * @param {string} alg - JSF algorithm identifier
33
- * @param {Object} [publicKeyJwk] - Optional JWK representation of the public key
34
- * @param {string} keyId - Key ID
188
+ * HMAC is only ever used with a key produced by this function (or any secret
189
+ * KeyObject). Public and private keys are refused, so a document cannot switch
190
+ * a verifier to HMAC and use its public key as the secret.
35
191
  *
36
- * @returns {Object} - JSF signature block
192
+ * @param {string|Buffer|crypto.KeyObject} material - Secret bytes, a UTF-8 string, or a secret KeyObject
193
+ * @returns {crypto.KeyObject} - A secret KeyObject
37
194
  */
38
- function createSignatureBlock(
39
- payload,
40
- privateKey,
41
- alg,
42
- publicKeyJwk = null,
43
- keyId = null,
44
- ) {
45
- // Exclude existing signatures from the canonicalized payload as per JSF
46
- const { signature, ...dataToSign } = payload;
47
- const canonicalData = canonicalize(dataToSign);
48
-
49
- let hashAlg;
50
- const signOptions = { key: privateKey };
51
-
52
- // Handle HMAC (Symmetric)
53
- if (alg.startsWith("HS")) {
54
- const hash = alg.replace("HS", "sha");
55
- const value = crypto
56
- .createHmac(hash, privateKey)
57
- .update(canonicalData, "utf8")
58
- .digest("base64url");
59
- const block = { algorithm: alg, value };
60
- if (publicKeyJwk) {
61
- block.publicKey = publicKeyJwk;
195
+ export function loadSharedSecret(material) {
196
+ if (material instanceof crypto.KeyObject) {
197
+ if (material.type !== "secret") {
198
+ throw new Error(
199
+ "HMAC signatures need a shared secret, but an asymmetric key was supplied.",
200
+ );
62
201
  }
63
- if (keyId) {
64
- block.keyId = keyId;
202
+ return material;
203
+ }
204
+ if (typeof material !== "string" && !Buffer.isBuffer(material)) {
205
+ throw new TypeError(
206
+ "The shared secret must be a string, a Buffer, or a secret KeyObject.",
207
+ );
208
+ }
209
+ if (looksLikeAsymmetricKey(material)) {
210
+ throw new Error(
211
+ "The shared secret looks like a public or private key. HMAC signatures must use a separate shared secret.",
212
+ );
213
+ }
214
+ const bytes =
215
+ typeof material === "string" ? Buffer.from(material, "utf8") : material;
216
+ if (!bytes.length) {
217
+ throw new Error("The shared secret is empty.");
218
+ }
219
+ return crypto.createSecretKey(bytes);
220
+ }
221
+
222
+ /**
223
+ * Loads the key used to verify signatures.
224
+ *
225
+ * Strings and Buffers are always parsed as asymmetric keys (a private key is
226
+ * reduced to its public half). To verify HMAC signatures, pass the result of
227
+ * {@link loadSharedSecret} instead.
228
+ *
229
+ * @param {string|Buffer|Object|crypto.KeyObject} key - PEM text, a createPublicKey() input, or a KeyObject
230
+ * @returns {crypto.KeyObject} - A public or secret KeyObject
231
+ */
232
+ export function loadVerificationKey(key) {
233
+ if (key instanceof crypto.KeyObject) {
234
+ return key.type === "private" ? crypto.createPublicKey(key) : key;
235
+ }
236
+ try {
237
+ return crypto.createPublicKey(key);
238
+ } catch (err) {
239
+ throw new Error(`Unable to load the verification key: ${err.message}`);
240
+ }
241
+ }
242
+
243
+ function loadSigningKey(privateKey, alg, spec) {
244
+ let key;
245
+ if (spec.keyType === "secret") {
246
+ key = loadSharedSecret(privateKey);
247
+ } else if (privateKey instanceof crypto.KeyObject) {
248
+ key = privateKey;
249
+ } else {
250
+ try {
251
+ key = crypto.createPrivateKey(privateKey);
252
+ } catch (err) {
253
+ throw new Error(`Unable to load the signing key: ${err.message}`);
65
254
  }
66
- return block;
67
- }
68
-
69
- // Handle Asymmetric Algorithms
70
- if (alg.startsWith("RS")) {
71
- hashAlg = alg.replace("RS", "SHA");
72
- } else if (alg.startsWith("PS")) {
73
- hashAlg = alg.replace("PS", "SHA");
74
- signOptions.padding = crypto.constants.RSA_PKCS1_PSS_PADDING;
75
- signOptions.saltLength = crypto.constants.RSA_PSS_SALTLEN_AUTO;
76
- } else if (alg.startsWith("ES")) {
77
- hashAlg = alg.replace("ES", "SHA");
255
+ }
256
+ assertKeyMatchesAlgorithm(key, alg, spec);
257
+ if (spec.keyType !== "secret" && key.type !== "private") {
258
+ throw new Error(
259
+ `Signing with ${alg} needs a private key, but a ${key.type} key was supplied.`,
260
+ );
261
+ }
262
+ return key;
263
+ }
264
+
265
+ /**
266
+ * Generates a PEM key pair suitable for the given JSF algorithm.
267
+ *
268
+ * @param {string} algorithm - JSF algorithm identifier
269
+ * @returns {{ publicKey: string, privateKey: string }} - PEM encoded SPKI public key and PKCS#8 private key
270
+ */
271
+ export function generateSigningKeyPair(algorithm) {
272
+ const spec = getAlgorithm(algorithm);
273
+ const encoding = {
274
+ publicKeyEncoding: { type: "spki", format: "pem" },
275
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
276
+ };
277
+ switch (spec.keyType) {
278
+ case "secret":
279
+ throw new Error(
280
+ `${algorithm} uses a shared secret, so there is no key pair to generate.`,
281
+ );
282
+ case "rsa":
283
+ case "rsa-pss":
284
+ return crypto.generateKeyPairSync("rsa", {
285
+ modulusLength: 4096,
286
+ ...encoding,
287
+ });
288
+ case "ec":
289
+ return crypto.generateKeyPairSync("ec", {
290
+ namedCurve: spec.curve,
291
+ ...encoding,
292
+ });
293
+ default:
294
+ return crypto.generateKeyPairSync(spec.keyType, encoding);
295
+ }
296
+ }
297
+
298
+ function signatureOptions(key, spec) {
299
+ const options = { key };
300
+ if (spec.keyType === "rsa-pss") {
301
+ options.padding = crypto.constants.RSA_PKCS1_PSS_PADDING;
302
+ // JWA fixes the PSS salt length to the digest length.
303
+ options.saltLength = crypto.constants.RSA_PSS_SALTLEN_DIGEST;
304
+ } else if (spec.keyType === "ec") {
78
305
  // Standard JWA format requires IEEE P1363 (R || S) instead of ASN.1 DER
79
- signOptions.dsaEncoding = "ieee-p1363";
80
- } else if (alg === "Ed25519" || alg === "Ed448") {
81
- // Native EdDSA algorithms do not require a separate hash algorithm definition
82
- hashAlg = null;
83
- } else {
84
- throw new Error(`Unsupported JSF algorithm: ${alg}`);
306
+ options.dsaEncoding = "ieee-p1363";
307
+ }
308
+ return options;
309
+ }
310
+
311
+ function computeSignature(canonicalData, key, spec) {
312
+ const data = Buffer.from(canonicalData, "utf8");
313
+ if (spec.keyType === "secret") {
314
+ return crypto.createHmac(spec.hash, key).update(data).digest();
85
315
  }
86
- const sigBuffer = crypto.sign(
87
- hashAlg,
88
- Buffer.from(canonicalData, "utf8"),
89
- signOptions,
316
+ return crypto.sign(spec.hash, data, signatureOptions(key, spec));
317
+ }
318
+
319
+ function signatureMatches(canonicalData, key, spec, signature) {
320
+ const data = Buffer.from(canonicalData, "utf8");
321
+ if (spec.keyType === "secret") {
322
+ const expected = crypto.createHmac(spec.hash, key).update(data).digest();
323
+ return (
324
+ signature.length === expected.length &&
325
+ crypto.timingSafeEqual(signature, expected)
326
+ );
327
+ }
328
+ return crypto.verify(spec.hash, data, signatureOptions(key, spec), signature);
329
+ }
330
+
331
+ // KeyObject.equals() on keys of different types leaves an OpenSSL error queued,
332
+ // which the next unrelated key operation in the process then throws. Compare
333
+ // the key types first so a hostile embedded key cannot disturb later checks.
334
+ function isSameKey(a, b) {
335
+ return (
336
+ a.type === b.type &&
337
+ a.asymmetricKeyType === b.asymmetricKeyType &&
338
+ a.asymmetricKeyDetails?.namedCurve === b.asymmetricKeyDetails?.namedCurve &&
339
+ a.equals(b)
90
340
  );
91
- const block = { algorithm: alg, value: sigBuffer.toString("base64url") };
92
- if (publicKeyJwk) {
93
- block.publicKey = publicKeyJwk;
341
+ }
342
+
343
+ function publicKeyFromJwk(jwk) {
344
+ if (!isPlainObject(jwk)) {
345
+ throw new Error("The signature publicKey must be a JWK object.");
346
+ }
347
+ return crypto.createPublicKey({ key: jwk, format: "jwk" });
348
+ }
349
+
350
+ function embeddablePublicKey(publicKeyJwk, signingKey, alg) {
351
+ if (!publicKeyJwk) {
352
+ return undefined;
353
+ }
354
+ if (signingKey.type === "secret") {
355
+ throw new Error(
356
+ `${alg} signatures use a shared secret, so a public key cannot be embedded.`,
357
+ );
94
358
  }
95
- if (keyId) {
96
- block.keyId = keyId;
359
+ const derived = crypto.createPublicKey(signingKey);
360
+ if (!isSameKey(publicKeyFromJwk(publicKeyJwk), derived)) {
361
+ throw new Error(
362
+ "The public key to embed in the signature does not belong to the signing key.",
363
+ );
97
364
  }
98
- return block;
365
+ // Export from the derived key so that only public members are ever embedded.
366
+ return derived.export({ format: "jwk" });
99
367
  }
100
368
 
101
369
  /**
102
- * Appends or replaces a signature on a target object based on the configured mode.
370
+ * Splits a JSF signature property into its signature objects.
371
+ *
372
+ * @returns {{ form: "single"|"signers"|"chain", entries: Object[], wrapper: Object }}
103
373
  */
104
- function addSignature(target, sigBlock, mode) {
105
- if (!target.signature) {
106
- target.signature = sigBlock;
107
- return;
108
- }
109
- if (mode === "chain") {
110
- if (target.signature.chain) {
111
- target.signature.chain.push(sigBlock);
112
- } else if (target.signature.signers) {
113
- throw new Error("Cannot mix signature chains and multi-signers.");
114
- } else {
115
- target.signature = { chain: [target.signature, sigBlock] };
374
+ function readSignatureEntries(signature) {
375
+ if (!isPlainObject(signature)) {
376
+ throw new Error("The signature must be a JSON object.");
377
+ }
378
+ const hasSigners = Object.hasOwn(signature, "signers");
379
+ const hasChain = Object.hasOwn(signature, "chain");
380
+ if (hasSigners && hasChain) {
381
+ throw new Error("A signature cannot contain both signers and chain.");
382
+ }
383
+ if (!hasSigners && !hasChain) {
384
+ return { form: "single", entries: [signature], wrapper: {} };
385
+ }
386
+ const form = hasSigners ? "signers" : "chain";
387
+ const { [form]: entries, ...wrapper } = signature;
388
+ if (
389
+ !Array.isArray(entries) ||
390
+ !entries.length ||
391
+ !entries.every(isPlainObject)
392
+ ) {
393
+ throw new Error(`The ${form} array must contain signature objects.`);
394
+ }
395
+ return { form, entries, wrapper };
396
+ }
397
+
398
+ function withoutValue(entry) {
399
+ const { value: _value, ...rest } = entry;
400
+ return rest;
401
+ }
402
+
403
+ /**
404
+ * Builds the object whose canonical form is signed for entry `index`.
405
+ *
406
+ * Following JSF, only the `value` of the entry being processed is removed.
407
+ * Other `signers` are removed entirely, while a `chain` entry keeps every
408
+ * earlier entry (including its value) and drops the later ones.
409
+ */
410
+ function jsfSignedView(content, form, entries, wrapper, index) {
411
+ if (form === "single") {
412
+ return { ...content, signature: withoutValue(entries[index]) };
413
+ }
414
+ const kept =
415
+ form === "signers"
416
+ ? [withoutValue(entries[index])]
417
+ : [...entries.slice(0, index), withoutValue(entries[index])];
418
+ return { ...content, signature: { ...wrapper, [form]: kept } };
419
+ }
420
+
421
+ function candidateViews(content, parsed, index) {
422
+ const views = [
423
+ jsfSignedView(content, parsed.form, parsed.entries, parsed.wrapper, index),
424
+ ];
425
+ // Appending a signers or chain entry to a single signature moves that
426
+ // signature into the new array. It was signed in the single signature form,
427
+ // which asserts the same thing as a signers entry at any position, and as a
428
+ // chain entry at position 0, where nothing precedes it.
429
+ if (
430
+ (parsed.form === "signers" || (parsed.form === "chain" && index === 0)) &&
431
+ !Object.keys(parsed.wrapper).length
432
+ ) {
433
+ views.push(jsfSignedView(content, "single", parsed.entries, {}, index));
434
+ }
435
+ return views;
436
+ }
437
+
438
+ function decodeSignatureValue(value) {
439
+ if (typeof value !== "string" || !value.length) {
440
+ throw new Error("The signature value is missing.");
441
+ }
442
+ const signature = Buffer.from(value, "base64url");
443
+ if (signature.toString("base64url") !== value) {
444
+ throw new Error("The signature value is not canonical base64url.");
445
+ }
446
+ return signature;
447
+ }
448
+
449
+ function verifyEntry(content, parsed, index, key) {
450
+ const entry = parsed.entries[index];
451
+ const spec = getAlgorithm(entry.algorithm);
452
+ const signature = decodeSignatureValue(entry.value);
453
+ if (
454
+ Object.hasOwn(entry, "excludes") ||
455
+ Object.hasOwn(parsed.wrapper, "excludes")
456
+ ) {
457
+ throw new Error("Signatures that use JSF excludes are not supported.");
458
+ }
459
+ assertKeyMatchesAlgorithm(key, entry.algorithm, spec);
460
+ if (entry.publicKey !== undefined) {
461
+ if (key.type === "secret") {
462
+ throw new Error("HMAC signatures must not embed a public key.");
116
463
  }
117
- } else if (mode === "signers") {
118
- if (target.signature.signers) {
119
- target.signature.signers.push(sigBlock);
120
- } else if (target.signature.chain) {
121
- throw new Error("Cannot mix signature chains and multi-signers.");
122
- } else {
123
- target.signature = { signers: [target.signature, sigBlock] };
464
+ if (!isSameKey(publicKeyFromJwk(entry.publicKey), key)) {
465
+ throw new Error(
466
+ "The embedded publicKey does not match the verification key.",
467
+ );
124
468
  }
125
- } else {
126
- target.signature = sigBlock;
127
469
  }
470
+ return candidateViews(content, parsed, index).some((view) =>
471
+ signatureMatches(canonicalize(view), key, spec, signature),
472
+ );
473
+ }
474
+
475
+ function mismatchReason(entry) {
476
+ return `The ${entry.algorithm} signature does not verify with this key: the signed content changed, or a different key made it.`;
477
+ }
478
+
479
+ function signatureLayout(target, mode) {
480
+ const existing = target.signature;
481
+ if (mode === "replace" || existing === undefined) {
482
+ return {
483
+ form: mode === "replace" ? "single" : mode,
484
+ entries: [],
485
+ wrapper: {},
486
+ };
487
+ }
488
+ const parsed = readSignatureEntries(existing);
489
+ if (parsed.form === "single") {
490
+ return { form: mode, entries: parsed.entries, wrapper: {} };
491
+ }
492
+ if (parsed.form !== mode) {
493
+ throw new Error("Cannot mix signature chains and multi-signers.");
494
+ }
495
+ return parsed;
496
+ }
497
+
498
+ function addSignature(target, key, spec, template, layout) {
499
+ const { signature: _existing, ...content } = target;
500
+ const entry = structuredClone(template);
501
+ const entries = [...layout.entries, entry];
502
+ const index = entries.length - 1;
503
+ const view = jsfSignedView(
504
+ content,
505
+ layout.form,
506
+ entries,
507
+ layout.wrapper,
508
+ index,
509
+ );
510
+ entry.value = computeSignature(canonicalize(view), key, spec).toString(
511
+ "base64url",
512
+ );
513
+ target.signature =
514
+ layout.form === "single"
515
+ ? entry
516
+ : { ...layout.wrapper, [layout.form]: entries };
128
517
  }
129
518
 
130
519
  /**
131
- * Recursively applies signatures to the BOM and its granular components.
520
+ * Applies JSF signatures to the BOM and, optionally, its components, services,
521
+ * and annotations.
522
+ *
523
+ * Nested elements are signed by default. When a `signers` or `chain` signature
524
+ * is appended to a BOM that already has a root signature, nested elements are
525
+ * skipped by default, because changing them would invalidate the existing root
526
+ * signature; explicitly requesting nested signing in that case is an error.
132
527
  *
133
528
  * @param {Object} bomJson - CycloneDX BOM Object
134
- * @param {Object} options - Signing options { privateKey, algorithm, mode, ... }
529
+ * @param {Object} options - Signing options
530
+ * @param {string|Buffer|Object|crypto.KeyObject} options.privateKey - Private key (PEM or KeyObject), or the shared secret for HS* algorithms
531
+ * @param {string} [options.algorithm] - JSF algorithm identifier (default RS512). It must match the key type.
532
+ * @param {Object} [options.publicKeyJwk] - JWK of the signing key's public key to embed
533
+ * @param {string} [options.keyId] - Key ID to embed
534
+ * @param {string} [options.mode] - 'replace' (default), 'signers', or 'chain'
535
+ * @param {boolean} [options.signComponents] - Sign each component
536
+ * @param {boolean} [options.signServices] - Sign each service
537
+ * @param {boolean} [options.signAnnotations] - Sign each annotation
135
538
  * @returns {Object} - Signed BOM Object
136
539
  */
137
540
  export function signBom(bomJson, options = {}) {
@@ -140,173 +543,201 @@ export function signBom(bomJson, options = {}) {
140
543
  algorithm = "RS512",
141
544
  publicKeyJwk = null,
142
545
  keyId = null,
143
- mode = "replace", // Supports: 'replace', 'chain', 'signers'
144
- signComponents = true,
145
- signServices = true,
146
- signAnnotations = true,
546
+ mode = "replace",
147
547
  } = options;
148
548
 
149
549
  if (!privateKey) {
150
550
  throw new Error("privateKey is required for signing");
151
551
  }
152
- if (signComponents && Array.isArray(bomJson.components)) {
153
- for (const comp of bomJson.components) {
154
- addSignature(
155
- comp,
156
- createSignatureBlock(comp, privateKey, algorithm, publicKeyJwk, keyId),
157
- mode,
158
- );
159
- }
552
+ if (!SIGNATURE_MODES.includes(mode)) {
553
+ throw new Error(
554
+ `Unsupported signature mode: ${mode}. Use one of ${SIGNATURE_MODES.join(", ")}.`,
555
+ );
160
556
  }
161
- if (signServices && Array.isArray(bomJson.services)) {
162
- for (const svc of bomJson.services) {
163
- addSignature(
164
- svc,
165
- createSignatureBlock(svc, privateKey, algorithm, publicKeyJwk, keyId),
166
- mode,
167
- );
168
- }
557
+ const spec = getAlgorithm(algorithm);
558
+ const signingKey = loadSigningKey(privateKey, algorithm, spec);
559
+ const template = { algorithm };
560
+ const publicKey = embeddablePublicKey(publicKeyJwk, signingKey, algorithm);
561
+ if (publicKey) {
562
+ template.publicKey = publicKey;
169
563
  }
170
- if (signAnnotations && Array.isArray(bomJson.annotations)) {
171
- for (const ann of bomJson.annotations) {
172
- addSignature(
173
- ann,
174
- createSignatureBlock(ann, privateKey, algorithm, publicKeyJwk, keyId),
175
- mode,
564
+ if (keyId !== null && keyId !== undefined && keyId !== "") {
565
+ template.keyId = String(keyId);
566
+ }
567
+
568
+ const appending = mode !== "replace" && bomJson.signature !== undefined;
569
+ const requested = {
570
+ components: options.signComponents,
571
+ services: options.signServices,
572
+ annotations: options.signAnnotations,
573
+ };
574
+ // Work out every signature layout before signing anything, so a rejected
575
+ // request leaves the BOM untouched.
576
+ const plan = [];
577
+ for (const field of NESTED_TARGETS) {
578
+ const items = Array.isArray(bomJson[field]) ? bomJson[field] : [];
579
+ const enabled =
580
+ requested[field] === undefined ? !appending : Boolean(requested[field]);
581
+ if (!enabled || !items.length) {
582
+ continue;
583
+ }
584
+ if (appending) {
585
+ throw new Error(
586
+ `Cannot sign ${field} while appending a ${mode} signature, because that changes content covered by the existing root signature. Sign the root only (for example, --no-sign-${field}).`,
176
587
  );
177
588
  }
589
+ for (const item of items) {
590
+ if (isPlainObject(item)) {
591
+ plan.push([item, signatureLayout(item, mode)]);
592
+ }
593
+ }
178
594
  }
179
- addSignature(
180
- bomJson,
181
- createSignatureBlock(bomJson, privateKey, algorithm, publicKeyJwk, keyId),
182
- mode,
183
- );
595
+ const rootLayout = signatureLayout(bomJson, mode);
596
+ for (const [item, layout] of plan) {
597
+ addSignature(item, signingKey, spec, template, layout);
598
+ }
599
+ addSignature(bomJson, signingKey, spec, template, rootLayout);
184
600
  return bomJson;
185
601
  }
186
602
 
187
603
  /**
188
- * Validates a single JSF signature block against the payload.
604
+ * Verifies the signature of a single node (e.g., BOM root, Component, Service,
605
+ * Annotation). Handles single signatures, multi-signatures (signers), and
606
+ * signature chains, and returns the first signature object that matches the key.
189
607
  *
190
- * @param {Object} payload - The payload to verify
191
- * @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
192
- * @param {Object} sigBlock Signature
193
- *
194
- * @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
195
- */
196
- function verifySignatureBlock(payload, publicKey, sigBlock) {
197
- const { signature, ...dataToVerify } = payload;
198
- const canonicalData = canonicalize(dataToVerify);
199
-
200
- const { algorithm: alg, value } = sigBlock;
201
-
202
- if (alg.startsWith("HS")) {
203
- const hash = alg.replace("HS", "sha");
204
- const expected = crypto
205
- .createHmac(hash, publicKey)
206
- .update(canonicalData, "utf8")
207
- .digest("base64url");
208
- const isValid = expected === value;
209
- return isValid ? sigBlock : false;
210
- }
211
-
212
- const verifyOptions = { key: publicKey };
213
- let hashAlg;
214
-
215
- if (alg.startsWith("RS")) {
216
- hashAlg = alg.replace("RS", "SHA");
217
- } else if (alg.startsWith("PS")) {
218
- hashAlg = alg.replace("PS", "SHA");
219
- verifyOptions.padding = crypto.constants.RSA_PKCS1_PSS_PADDING;
220
- verifyOptions.saltLength = crypto.constants.RSA_PSS_SALTLEN_AUTO;
221
- } else if (alg.startsWith("ES")) {
222
- hashAlg = alg.replace("ES", "SHA");
223
- verifyOptions.dsaEncoding = "ieee-p1363";
224
- } else if (alg === "Ed25519" || alg === "Ed448") {
225
- hashAlg = null;
226
- } else {
227
- console.warn(`${alg} is unknown.`);
228
- return false;
229
- }
230
-
231
- const isValid = crypto.verify(
232
- hashAlg,
233
- Buffer.from(canonicalData, "utf8"),
234
- verifyOptions,
235
- Buffer.from(value, "base64url"),
236
- );
237
- return isValid ? sigBlock : false;
238
- }
239
-
240
- /**
241
- * Verifies the integrity of a specific element node (e.g., BOM root, Component, Service, Annotation).
242
- * Resolves standard JSF signatures, multisignature (signers), and chains.
608
+ * A signature only matches when its declared algorithm fits the key type, its
609
+ * embedded publicKey (if any) is the verification key, and its value covers the
610
+ * node content plus the signature metadata as JSF defines.
243
611
  *
244
612
  * @param {Object} node - The BOM or granular object to verify
245
- * @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
246
- * @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
613
+ * @param {string|Buffer|Object|crypto.KeyObject} publicKey - Verification key. Strings and Buffers are parsed as public keys; use loadSharedSecret() for HMAC.
614
+ * @param {Object} [options]
615
+ * @param {string[]} [options.reasons] - When no signature matches, receives the reason each entry was rejected
616
+ * @returns {boolean|Object} - Matching signature object if valid. False otherwise.
247
617
  */
248
- export function verifyNode(node, publicKey) {
249
- if (!node?.signature) {
618
+ export function verifyNode(node, publicKey, options = {}) {
619
+ if (!isPlainObject(node) || node.signature === undefined) {
250
620
  return false;
251
621
  }
252
- const sigTarget = node.signature;
253
- if (sigTarget.signers) {
254
- for (const sig of sigTarget.signers) {
255
- const match = verifySignatureBlock(node, publicKey, sig);
256
- if (match) {
257
- return match;
258
- }
259
- }
622
+ const key = loadVerificationKey(publicKey);
623
+ const reasons = Array.isArray(options.reasons) ? options.reasons : undefined;
624
+ let parsed;
625
+ try {
626
+ parsed = readSignatureEntries(node.signature);
627
+ } catch (err) {
628
+ reasons?.push(err.message);
260
629
  return false;
261
630
  }
262
- if (sigTarget.chain) {
263
- for (const sig of sigTarget.chain) {
264
- const match = verifySignatureBlock(node, publicKey, sig);
265
- if (match) {
266
- return match;
631
+ const { signature: _signature, ...content } = node;
632
+ const rejected = [];
633
+ for (let index = 0; index < parsed.entries.length; index++) {
634
+ const label = parsed.form === "single" ? "" : `${parsed.form}[${index}]: `;
635
+ try {
636
+ if (verifyEntry(content, parsed, index, key)) {
637
+ return parsed.entries[index];
267
638
  }
639
+ rejected.push(`${label}${mismatchReason(parsed.entries[index])}`);
640
+ } catch (err) {
641
+ rejected.push(`${label}${err.message}`);
268
642
  }
269
- return false;
270
643
  }
271
- return verifySignatureBlock(node, publicKey, sigTarget);
644
+ reasons?.push(...rejected);
645
+ return false;
272
646
  }
273
647
 
274
648
  /**
275
- * Verifies the integrity of a BOM's top-level signature, as well as nested components, services, and annotations.
276
- * Returns true only if the root signature is valid AND all signed nested elements are valid.
649
+ * Verifies a BOM's top-level signature, as well as nested components, services,
650
+ * and annotations. Returns the root match only if the root signature is valid
651
+ * AND every signed nested element is valid for the same key.
277
652
  *
278
653
  * @param {Object} bom - CycloneDX BOM Object
279
- * @param {string|crypto.KeyObject} publicKey - The public key corresponding to the signature
280
- * @returns {boolean|Object} - Signature block if signature is valid. False otherwise.
654
+ * @param {string|Buffer|Object|crypto.KeyObject} publicKey - Verification key (see verifyNode)
655
+ * @param {Object} [options] - Same options as verifyNode
656
+ * @returns {boolean|Object} - Signature object if valid. False otherwise.
281
657
  */
282
- export function verifyBom(bom, publicKey) {
658
+ export function verifyBom(bom, publicKey, options = {}) {
283
659
  if (!bom?.signature) {
284
660
  return false;
285
661
  }
286
- const rootMatch = verifyNode(bom, publicKey);
662
+ const key = loadVerificationKey(publicKey);
663
+ const reasons = Array.isArray(options.reasons) ? options.reasons : undefined;
664
+ const rootMatch = verifyNode(bom, key, options);
287
665
  if (!rootMatch) {
288
666
  return false;
289
667
  }
290
- if (Array.isArray(bom.components)) {
291
- for (const comp of bom.components) {
292
- if (comp.signature && !verifyNode(comp, publicKey)) {
293
- return false;
294
- }
668
+ for (const field of NESTED_TARGETS) {
669
+ if (!Array.isArray(bom[field])) {
670
+ continue;
295
671
  }
296
- }
297
- if (Array.isArray(bom.services)) {
298
- for (const svc of bom.services) {
299
- if (svc.signature && !verifyNode(svc, publicKey)) {
300
- return false;
672
+ for (const [index, item] of bom[field].entries()) {
673
+ if (!item?.signature) {
674
+ continue;
301
675
  }
302
- }
303
- }
304
- if (Array.isArray(bom.annotations)) {
305
- for (const ann of bom.annotations) {
306
- if (ann.signature && !verifyNode(ann, publicKey)) {
676
+ const nestedReasons = [];
677
+ if (!verifyNode(item, key, { ...options, reasons: nestedReasons })) {
678
+ const name = displayValue(
679
+ item["bom-ref"] || item.name || item.subject || index,
680
+ );
681
+ for (const reason of nestedReasons) {
682
+ reasons?.push(`${field}[${index}] '${name}': ${reason}`);
683
+ }
307
684
  return false;
308
685
  }
309
686
  }
310
687
  }
311
688
  return rootMatch;
312
689
  }
690
+
691
+ /**
692
+ * Checks every signature entry of a node against a set of trusted keys. An
693
+ * entry is verified when one of the keys verifies that entry.
694
+ *
695
+ * `cdx-sign` uses this before appending to a signature chain, because the new
696
+ * chain entry vouches for every earlier entry.
697
+ *
698
+ * @param {Object} node - The BOM or granular object whose signature is checked
699
+ * @param {Array<string|Buffer|Object|crypto.KeyObject>} [keys] - Trusted verification keys (see verifyNode)
700
+ * @returns {Array<{ index: number, algorithm: any, keyId: any, verified: boolean, reasons: string[] }>} - One result per signature entry; empty for an unsigned node
701
+ */
702
+ export function checkSignatureEntries(node, keys = []) {
703
+ if (!isPlainObject(node) || node.signature === undefined) {
704
+ return [];
705
+ }
706
+ const trustedKeys = keys.map((key) => loadVerificationKey(key));
707
+ let parsed;
708
+ try {
709
+ parsed = readSignatureEntries(node.signature);
710
+ } catch (err) {
711
+ return [
712
+ {
713
+ index: 0,
714
+ algorithm: undefined,
715
+ keyId: undefined,
716
+ verified: false,
717
+ reasons: [err.message],
718
+ },
719
+ ];
720
+ }
721
+ const { signature: _signature, ...content } = node;
722
+ return parsed.entries.map((entry, index) => {
723
+ const reasons = [];
724
+ const verified = trustedKeys.some((key) => {
725
+ try {
726
+ if (verifyEntry(content, parsed, index, key)) {
727
+ return true;
728
+ }
729
+ reasons.push(mismatchReason(entry));
730
+ } catch (err) {
731
+ reasons.push(err.message);
732
+ }
733
+ return false;
734
+ });
735
+ return {
736
+ index,
737
+ algorithm: entry.algorithm,
738
+ keyId: entry.keyId,
739
+ verified,
740
+ reasons: verified ? [] : [...new Set(reasons)],
741
+ };
742
+ });
743
+ }