@cyclonedx/cdxgen 12.1.5 → 12.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -40
- package/bin/cdxgen.js +194 -97
- package/bin/evinse.js +4 -4
- package/bin/repl.js +1 -1
- package/bin/sign.js +102 -0
- package/bin/validate.js +233 -0
- package/bin/verify.js +69 -28
- package/data/queries.json +1 -1
- package/data/rules/ci-permissions.yaml +186 -0
- package/data/rules/dependency-sources.yaml +123 -0
- package/data/rules/package-integrity.yaml +135 -0
- package/data/rules/vscode-extensions.yaml +228 -0
- package/lib/cli/index.js +449 -429
- package/lib/cli/index.poku.js +117 -0
- package/lib/evinser/db.js +137 -0
- package/lib/{helpers → evinser}/db.poku.js +2 -6
- package/lib/evinser/evinser.js +2 -14
- package/lib/helpers/analyzer.js +606 -3
- package/lib/helpers/analyzer.poku.js +230 -0
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/bomSigner.poku.js +156 -0
- package/lib/helpers/ciParsers/azurePipelines.js +295 -0
- package/lib/helpers/ciParsers/azurePipelines.poku.js +253 -0
- package/lib/helpers/ciParsers/circleCi.js +286 -0
- package/lib/helpers/ciParsers/circleCi.poku.js +230 -0
- package/lib/helpers/ciParsers/common.js +24 -0
- package/lib/helpers/ciParsers/githubActions.js +636 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +802 -0
- package/lib/helpers/ciParsers/gitlabCi.js +213 -0
- package/lib/helpers/ciParsers/gitlabCi.poku.js +247 -0
- package/lib/helpers/ciParsers/jenkins.js +181 -0
- package/lib/helpers/ciParsers/jenkins.poku.js +197 -0
- package/lib/helpers/depsUtils.js +219 -0
- package/lib/helpers/depsUtils.poku.js +207 -0
- package/lib/helpers/display.js +426 -5
- package/lib/helpers/envcontext.js +18 -3
- package/lib/helpers/formulationParsers.js +351 -0
- package/lib/helpers/logger.js +14 -0
- package/lib/helpers/protobom.js +9 -9
- package/lib/helpers/pythonutils.js +9 -0
- package/lib/helpers/remote/dependency-track.js +84 -0
- package/lib/helpers/remote/dependency-track.poku.js +119 -0
- package/lib/helpers/table.js +384 -0
- package/lib/helpers/table.poku.js +186 -0
- package/lib/helpers/utils.js +865 -416
- package/lib/helpers/utils.poku.js +172 -265
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/versutils.poku.js +315 -0
- package/lib/helpers/vsixutils.js +1061 -0
- package/lib/helpers/vsixutils.poku.js +2247 -0
- package/lib/managers/binary.js +19 -19
- package/lib/managers/docker.js +108 -1
- package/lib/managers/oci.js +10 -0
- package/lib/managers/piptree.js +3 -9
- package/lib/parsers/npmrc.js +17 -13
- package/lib/parsers/npmrc.poku.js +41 -5
- package/lib/server/openapi.yaml +34 -1
- package/lib/server/server.js +50 -13
- package/lib/server/server.poku.js +332 -144
- package/lib/stages/postgen/annotator.js +1 -1
- package/lib/stages/postgen/auditBom.js +196 -0
- package/lib/stages/postgen/auditBom.poku.js +378 -0
- package/lib/stages/postgen/postgen.js +54 -1
- package/lib/stages/postgen/postgen.poku.js +90 -1
- package/lib/stages/postgen/ruleEngine.js +369 -0
- package/lib/stages/pregen/envAudit.js +299 -0
- package/lib/stages/pregen/envAudit.poku.js +572 -0
- package/lib/stages/pregen/pregen.js +12 -8
- package/lib/{helpers/validator.js → validator/bomValidator.js} +107 -47
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceEngine.poku.js +168 -0
- package/lib/validator/complianceRules.js +1610 -0
- package/lib/validator/complianceRules.poku.js +328 -0
- package/lib/validator/index.js +222 -0
- package/lib/validator/index.poku.js +144 -0
- package/lib/validator/reporters/annotations.js +121 -0
- package/lib/validator/reporters/console.js +149 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/lib/validator/reporters.poku.js +150 -0
- package/package.json +8 -9
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/validate.d.ts +3 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/helpers/utils.d.ts +0 -1
- package/types/lib/cli/index.d.ts +49 -52
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/db.d.ts +34 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +63 -16
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +27 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts +17 -0
- package/types/lib/helpers/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/common.d.ts +11 -0
- package/types/lib/helpers/ciParsers/common.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts +34 -0
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts +17 -0
- package/types/lib/helpers/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts +17 -0
- package/types/lib/helpers/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/helpers/depsUtils.d.ts +21 -0
- package/types/lib/helpers/depsUtils.d.ts.map +1 -0
- package/types/lib/helpers/display.d.ts +111 -11
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/envcontext.d.ts +19 -7
- package/types/lib/helpers/envcontext.d.ts.map +1 -1
- package/types/lib/helpers/formulationParsers.d.ts +50 -0
- package/types/lib/helpers/formulationParsers.d.ts.map +1 -0
- package/types/lib/helpers/logger.d.ts +15 -1
- package/types/lib/helpers/logger.d.ts.map +1 -1
- package/types/lib/helpers/protobom.d.ts +2 -2
- package/types/lib/helpers/pythonutils.d.ts +10 -1
- package/types/lib/helpers/pythonutils.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +16 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/table.d.ts +6 -0
- package/types/lib/helpers/table.d.ts.map +1 -0
- package/types/lib/helpers/utils.d.ts +533 -128
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +12 -31
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/managers/oci.d.ts +11 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/managers/piptree.d.ts.map +1 -1
- package/types/lib/parsers/npmrc.d.ts +4 -1
- package/types/lib/parsers/npmrc.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +22 -2
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/auditBom.d.ts +20 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +8 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +8 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/{helpers/validator.d.ts → validator/bomValidator.d.ts} +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +66 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +70 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +70 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +31 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +30 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +21 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +16 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/lib/helpers/db.js +0 -162
- package/lib/stages/pregen/env-audit.js +0 -34
- package/lib/stages/pregen/env-audit.poku.js +0 -290
- package/types/helpers/db.d.ts +0 -35
- package/types/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/db.d.ts +0 -35
- package/types/lib/helpers/db.d.ts.map +0 -1
- package/types/lib/helpers/validator.d.ts.map +0 -1
- package/types/lib/stages/pregen/env-audit.d.ts +0 -2
- package/types/lib/stages/pregen/env-audit.d.ts.map +0 -1
- package/types/managers/binary.d.ts +0 -37
- package/types/managers/binary.d.ts.map +0 -1
- package/types/managers/docker.d.ts +0 -56
- package/types/managers/docker.d.ts.map +0 -1
- package/types/managers/oci.d.ts +0 -2
- package/types/managers/oci.d.ts.map +0 -1
- package/types/managers/piptree.d.ts +0 -2
- package/types/managers/piptree.d.ts.map +0 -1
- package/types/server/server.d.ts +0 -34
- package/types/server/server.d.ts.map +0 -1
- package/types/stages/postgen/annotator.d.ts +0 -27
- package/types/stages/postgen/annotator.d.ts.map +0 -1
- package/types/stages/postgen/postgen.d.ts +0 -51
- package/types/stages/postgen/postgen.d.ts.map +0 -1
- package/types/stages/pregen/pregen.d.ts +0 -59
- package/types/stages/pregen/pregen.d.ts.map +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import {
|
|
2
|
+
copyFileSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { assert, describe, it } from "poku";
|
|
12
|
+
|
|
13
|
+
import { findJSImportsExports } from "./analyzer.js";
|
|
14
|
+
|
|
15
|
+
const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-analyzer-poku-"));
|
|
16
|
+
|
|
17
|
+
process.on("exit", () => {
|
|
18
|
+
rmSync(baseTempDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const createProject = (subDirName, entryContent) => {
|
|
22
|
+
const projectDir = join(baseTempDir, subDirName);
|
|
23
|
+
mkdirSync(projectDir, { recursive: true });
|
|
24
|
+
writeFileSync(join(projectDir, "index.js"), entryContent, {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
});
|
|
27
|
+
return projectDir;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const createProjectFromFixture = (subDirName, fixtureFileName) => {
|
|
31
|
+
const projectDir = join(baseTempDir, subDirName);
|
|
32
|
+
mkdirSync(projectDir, { recursive: true });
|
|
33
|
+
const fixturePath = new URL(
|
|
34
|
+
`../../test/data/${fixtureFileName}`,
|
|
35
|
+
import.meta.url,
|
|
36
|
+
);
|
|
37
|
+
copyFileSync(fixturePath, join(projectDir, fixtureFileName));
|
|
38
|
+
return projectDir;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("findJSImportsExports() wasm and wasi detection", () => {
|
|
42
|
+
it("captures wasm exports from WebAssembly.instantiate() flow", async () => {
|
|
43
|
+
const projectDir = createProject(
|
|
44
|
+
"instantiate-flow",
|
|
45
|
+
`import fs from "node:fs/promises";
|
|
46
|
+
const wasmBuffer = await fs.readFile("./add.wasm");
|
|
47
|
+
const wasmModule = await WebAssembly.instantiate(wasmBuffer);
|
|
48
|
+
const { add } = wasmModule.instance.exports;
|
|
49
|
+
console.log(add(5, 6));
|
|
50
|
+
`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
54
|
+
assert.ok(allImports["add.wasm"], "expected add.wasm to be discovered");
|
|
55
|
+
const occurrences = Array.from(allImports["add.wasm"]);
|
|
56
|
+
assert.ok(
|
|
57
|
+
occurrences.some((occ) => occ.importedModules?.includes("add")),
|
|
58
|
+
"expected add export symbol to be tracked",
|
|
59
|
+
);
|
|
60
|
+
const addOccurrence = occurrences.find((occ) =>
|
|
61
|
+
occ.importedModules?.includes("add"),
|
|
62
|
+
);
|
|
63
|
+
assert.ok(addOccurrence, "expected add symbol occurrence to exist");
|
|
64
|
+
assert.ok(
|
|
65
|
+
addOccurrence.fileName?.includes("index.js"),
|
|
66
|
+
"expected source filename to be tracked",
|
|
67
|
+
);
|
|
68
|
+
assert.strictEqual(addOccurrence.lineNumber, 4);
|
|
69
|
+
assert.strictEqual(typeof addOccurrence.columnNumber, "number");
|
|
70
|
+
assert.ok(addOccurrence.columnNumber >= 0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("captures wasm exports from instantiateStreaming(fetch(new URL(...)))", async () => {
|
|
74
|
+
const projectDir = createProject(
|
|
75
|
+
"streaming-flow",
|
|
76
|
+
`const { instance } = await WebAssembly.instantiateStreaming(
|
|
77
|
+
fetch(new URL("./stream.wasm", import.meta.url)),
|
|
78
|
+
);
|
|
79
|
+
const { run } = instance.exports;
|
|
80
|
+
console.log(run());
|
|
81
|
+
`,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
85
|
+
assert.ok(
|
|
86
|
+
allImports["stream.wasm"],
|
|
87
|
+
"expected stream.wasm to be discovered",
|
|
88
|
+
);
|
|
89
|
+
const occurrences = Array.from(allImports["stream.wasm"]);
|
|
90
|
+
assert.ok(
|
|
91
|
+
occurrences.some((occ) => occ.importedModules?.includes("run")),
|
|
92
|
+
"expected run export symbol to be tracked",
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not treat arbitrary function calls with .wasm literals as wasm imports", async () => {
|
|
97
|
+
const projectDir = createProject(
|
|
98
|
+
"non-wasm-callee",
|
|
99
|
+
`doSomething("./ignored.wasm");
|
|
100
|
+
`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
104
|
+
assert.ok(
|
|
105
|
+
!allImports["./ignored.wasm"] && !allImports["ignored.wasm"],
|
|
106
|
+
"expected non-wasm callee usage to be ignored",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("captures wasi constructor and lifecycle API usage", async () => {
|
|
111
|
+
const projectDir = createProject(
|
|
112
|
+
"wasi-flow",
|
|
113
|
+
`import { WASI } from "node:wasi";
|
|
114
|
+
const wasi = new WASI({ version: "preview1" });
|
|
115
|
+
wasi.initialize(instance);
|
|
116
|
+
`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
120
|
+
assert.ok(allImports["node:wasi"], "expected node:wasi to be discovered");
|
|
121
|
+
const occurrences = Array.from(allImports["node:wasi"]);
|
|
122
|
+
assert.ok(
|
|
123
|
+
occurrences.some((occ) => occ.importedModules?.includes("WASI")),
|
|
124
|
+
"expected WASI usage to be tracked",
|
|
125
|
+
);
|
|
126
|
+
assert.ok(
|
|
127
|
+
occurrences.some((occ) => occ.importedModules?.includes("initialize")),
|
|
128
|
+
"expected initialize API usage to be tracked",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("captures wasi constructor alias invoked without new", async () => {
|
|
133
|
+
const projectDir = createProject(
|
|
134
|
+
"wasi-call-alias-flow",
|
|
135
|
+
`import { WASI as WasiCtor } from "node:wasi";
|
|
136
|
+
const wasi = WasiCtor({ version: "preview1" });
|
|
137
|
+
wasi.start(instance);
|
|
138
|
+
`,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const { allImports } = await findJSImportsExports(projectDir, false);
|
|
142
|
+
assert.ok(allImports["node:wasi"], "expected node:wasi to be discovered");
|
|
143
|
+
const occurrences = Array.from(allImports["node:wasi"]);
|
|
144
|
+
assert.ok(
|
|
145
|
+
occurrences.some((occ) => occ.importedModules?.includes("WASI")),
|
|
146
|
+
"expected WASI constructor alias usage to be tracked",
|
|
147
|
+
);
|
|
148
|
+
assert.ok(
|
|
149
|
+
occurrences.some((occ) => occ.importedModules?.includes("start")),
|
|
150
|
+
"expected start API usage to be tracked",
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("detects wasm import/export functions from libmagic wrapper fixture", async () => {
|
|
155
|
+
const projectDir = createProjectFromFixture(
|
|
156
|
+
"libmagic-wrapper",
|
|
157
|
+
"libmagic-wrapper.js",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const { allImports, allExports } = await findJSImportsExports(
|
|
161
|
+
projectDir,
|
|
162
|
+
false,
|
|
163
|
+
);
|
|
164
|
+
assert.ok(allImports.fs, "expected fs require import to be detected");
|
|
165
|
+
assert.ok(
|
|
166
|
+
allImports.crypto,
|
|
167
|
+
"expected crypto require import to be detected",
|
|
168
|
+
);
|
|
169
|
+
assert.ok(
|
|
170
|
+
allImports["libmagic-wrapper.wasm"],
|
|
171
|
+
"expected libmagic-wrapper.wasm to be detected",
|
|
172
|
+
);
|
|
173
|
+
assert.ok(
|
|
174
|
+
allExports["libmagic-wrapper.wasm"],
|
|
175
|
+
"expected libmagic-wrapper.wasm exports to be detected",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const wasmImportOccurrences = Array.from(
|
|
179
|
+
allImports["libmagic-wrapper.wasm"],
|
|
180
|
+
);
|
|
181
|
+
const wasmExportOccurrences = Array.from(
|
|
182
|
+
allExports["libmagic-wrapper.wasm"],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
assert.ok(
|
|
186
|
+
wasmImportOccurrences.some(
|
|
187
|
+
(occ) =>
|
|
188
|
+
occ.fileName?.includes("libmagic-wrapper.js") &&
|
|
189
|
+
typeof occ.lineNumber === "number" &&
|
|
190
|
+
typeof occ.columnNumber === "number",
|
|
191
|
+
),
|
|
192
|
+
"expected wasm import occurrences to include source location metadata",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const importedModules = new Set(
|
|
196
|
+
wasmImportOccurrences.flatMap((occ) => occ.importedModules || []),
|
|
197
|
+
);
|
|
198
|
+
for (const expectedImportedModule of [
|
|
199
|
+
"free",
|
|
200
|
+
"malloc",
|
|
201
|
+
"magic_wrapper_load",
|
|
202
|
+
"magic_wrapper_detect",
|
|
203
|
+
"_emscripten_stack_restore",
|
|
204
|
+
"_emscripten_stack_alloc",
|
|
205
|
+
"emscripten_stack_get_current",
|
|
206
|
+
"memory",
|
|
207
|
+
"__indirect_function_table",
|
|
208
|
+
]) {
|
|
209
|
+
assert.ok(
|
|
210
|
+
importedModules.has(expectedImportedModule),
|
|
211
|
+
`expected imported wasm symbol ${expectedImportedModule}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const exportedModules = new Set(
|
|
216
|
+
wasmExportOccurrences.flatMap((occ) => occ.exportedModules || []),
|
|
217
|
+
);
|
|
218
|
+
for (const expectedExportedModule of [
|
|
219
|
+
"_free",
|
|
220
|
+
"_malloc",
|
|
221
|
+
"_magic_wrapper_load",
|
|
222
|
+
"_magic_wrapper_detect",
|
|
223
|
+
]) {
|
|
224
|
+
assert.ok(
|
|
225
|
+
exportedModules.has(expectedExportedModule),
|
|
226
|
+
`expected exported wasm symbol ${expectedExportedModule}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight, deterministic JSON Canonicalizer (similar to RFC 8785).
|
|
5
|
+
* Required by JSF to ensure the signature payload remains identical across systems.
|
|
6
|
+
*
|
|
7
|
+
* @param {any} value - The JSON object/value to canonicalize
|
|
8
|
+
* @returns {string} - Canonicalized JSON string
|
|
9
|
+
*/
|
|
10
|
+
function canonicalize(value) {
|
|
11
|
+
if (value === null || typeof value !== "object") {
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
16
|
+
}
|
|
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]])}`;
|
|
22
|
+
}
|
|
23
|
+
str += "}";
|
|
24
|
+
return str;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a JSF-compliant signature block.
|
|
29
|
+
*
|
|
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
|
|
35
|
+
*
|
|
36
|
+
* @returns {Object} - JSF signature block
|
|
37
|
+
*/
|
|
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;
|
|
62
|
+
}
|
|
63
|
+
if (keyId) {
|
|
64
|
+
block.keyId = keyId;
|
|
65
|
+
}
|
|
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");
|
|
78
|
+
// 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}`);
|
|
85
|
+
}
|
|
86
|
+
const sigBuffer = crypto.sign(
|
|
87
|
+
hashAlg,
|
|
88
|
+
Buffer.from(canonicalData, "utf8"),
|
|
89
|
+
signOptions,
|
|
90
|
+
);
|
|
91
|
+
const block = { algorithm: alg, value: sigBuffer.toString("base64url") };
|
|
92
|
+
if (publicKeyJwk) {
|
|
93
|
+
block.publicKey = publicKeyJwk;
|
|
94
|
+
}
|
|
95
|
+
if (keyId) {
|
|
96
|
+
block.keyId = keyId;
|
|
97
|
+
}
|
|
98
|
+
return block;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Appends or replaces a signature on a target object based on the configured mode.
|
|
103
|
+
*/
|
|
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] };
|
|
116
|
+
}
|
|
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] };
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
target.signature = sigBlock;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Recursively applies signatures to the BOM and its granular components.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} bomJson - CycloneDX BOM Object
|
|
134
|
+
* @param {Object} options - Signing options { privateKey, algorithm, mode, ... }
|
|
135
|
+
* @returns {Object} - Signed BOM Object
|
|
136
|
+
*/
|
|
137
|
+
export function signBom(bomJson, options = {}) {
|
|
138
|
+
const {
|
|
139
|
+
privateKey,
|
|
140
|
+
algorithm = "RS512",
|
|
141
|
+
publicKeyJwk = null,
|
|
142
|
+
keyId = null,
|
|
143
|
+
mode = "replace", // Supports: 'replace', 'chain', 'signers'
|
|
144
|
+
signComponents = true,
|
|
145
|
+
signServices = true,
|
|
146
|
+
signAnnotations = true,
|
|
147
|
+
} = options;
|
|
148
|
+
|
|
149
|
+
if (!privateKey) {
|
|
150
|
+
throw new Error("privateKey is required for signing");
|
|
151
|
+
}
|
|
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
|
+
}
|
|
160
|
+
}
|
|
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
|
+
}
|
|
169
|
+
}
|
|
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,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
addSignature(
|
|
180
|
+
bomJson,
|
|
181
|
+
createSignatureBlock(bomJson, privateKey, algorithm, publicKeyJwk, keyId),
|
|
182
|
+
mode,
|
|
183
|
+
);
|
|
184
|
+
return bomJson;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Validates a single JSF signature block against the payload.
|
|
189
|
+
*
|
|
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.
|
|
243
|
+
*
|
|
244
|
+
* @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.
|
|
247
|
+
*/
|
|
248
|
+
export function verifyNode(node, publicKey) {
|
|
249
|
+
if (!node?.signature) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
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
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (sigTarget.chain) {
|
|
263
|
+
for (const sig of sigTarget.chain) {
|
|
264
|
+
const match = verifySignatureBlock(node, publicKey, sig);
|
|
265
|
+
if (match) {
|
|
266
|
+
return match;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
return verifySignatureBlock(node, publicKey, sigTarget);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
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.
|
|
277
|
+
*
|
|
278
|
+
* @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.
|
|
281
|
+
*/
|
|
282
|
+
export function verifyBom(bom, publicKey) {
|
|
283
|
+
if (!bom?.signature) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const rootMatch = verifyNode(bom, publicKey);
|
|
287
|
+
if (!rootMatch) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(bom.components)) {
|
|
291
|
+
for (const comp of bom.components) {
|
|
292
|
+
if (comp.signature && !verifyNode(comp, publicKey)) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (Array.isArray(bom.services)) {
|
|
298
|
+
for (const svc of bom.services) {
|
|
299
|
+
if (svc.signature && !verifyNode(svc, publicKey)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(bom.annotations)) {
|
|
305
|
+
for (const ann of bom.annotations) {
|
|
306
|
+
if (ann.signature && !verifyNode(ann, publicKey)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return rootMatch;
|
|
312
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
|
|
4
|
+
import { describe, it } from "poku";
|
|
5
|
+
|
|
6
|
+
import { signBom, verifyBom, verifyNode } from "./bomSigner.js";
|
|
7
|
+
|
|
8
|
+
const rsaKeys = crypto.generateKeyPairSync("rsa", {
|
|
9
|
+
modulusLength: 2048,
|
|
10
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
11
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const ecKeys = crypto.generateKeyPairSync("ec", {
|
|
15
|
+
namedCurve: "prime256v1",
|
|
16
|
+
publicKeyEncoding: { type: "spki", format: "pem" },
|
|
17
|
+
privateKeyEncoding: { type: "pkcs8", format: "pem" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const generateMockBom = () => ({
|
|
21
|
+
bomFormat: "CycloneDX",
|
|
22
|
+
specVersion: "1.6",
|
|
23
|
+
components: [{ type: "library", name: "cdxgen", version: "1.0.0" }],
|
|
24
|
+
services: [{ name: "acme-service", endpoints: ["https://appthreat.com"] }],
|
|
25
|
+
annotations: [{ subject: "ref-1", annotator: { name: "System" } }],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("bomSigner Tests", async () => {
|
|
29
|
+
it("Test basic RS512 Signature & Verification", () => {
|
|
30
|
+
const bomRsa = generateMockBom();
|
|
31
|
+
const signedRsa = signBom(bomRsa, {
|
|
32
|
+
privateKey: rsaKeys.privateKey,
|
|
33
|
+
algorithm: "RS512",
|
|
34
|
+
});
|
|
35
|
+
assert.ok(signedRsa.signature, "Root BOM should be signed");
|
|
36
|
+
assert.strictEqual(signedRsa.signature.algorithm, "RS512");
|
|
37
|
+
assert.ok(
|
|
38
|
+
signedRsa.components[0].signature,
|
|
39
|
+
"Granular component should be signed",
|
|
40
|
+
);
|
|
41
|
+
assert.ok(
|
|
42
|
+
signedRsa.services[0].signature,
|
|
43
|
+
"Granular service should be signed",
|
|
44
|
+
);
|
|
45
|
+
assert.ok(
|
|
46
|
+
signedRsa.annotations[0].signature,
|
|
47
|
+
"Granular annotation should be signed",
|
|
48
|
+
);
|
|
49
|
+
assert.ok(verifyBom(signedRsa, rsaKeys.publicKey));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("Test ECDSA (ES256) Signature & Verification (JWA IEEE P1363 Format Compliance)", () => {
|
|
53
|
+
const bomEc = generateMockBom();
|
|
54
|
+
const signedEc = signBom(bomEc, {
|
|
55
|
+
privateKey: ecKeys.privateKey,
|
|
56
|
+
algorithm: "ES256",
|
|
57
|
+
});
|
|
58
|
+
assert.strictEqual(signedEc.signature.algorithm, "ES256");
|
|
59
|
+
assert.ok(verifyBom(signedEc, ecKeys.publicKey));
|
|
60
|
+
const signedRsa = signBom(bomEc, {
|
|
61
|
+
privateKey: rsaKeys.privateKey,
|
|
62
|
+
algorithm: "RS512",
|
|
63
|
+
});
|
|
64
|
+
assert.strictEqual(
|
|
65
|
+
verifyBom(signedRsa, ecKeys.publicKey),
|
|
66
|
+
false,
|
|
67
|
+
"Verification must fail with the wrong public key",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("Test Multi-Signature Support (`signers`)", () => {
|
|
72
|
+
const bomMulti = generateMockBom();
|
|
73
|
+
|
|
74
|
+
// 1st Pass: First signer signs the whole BOM including inner elements
|
|
75
|
+
signBom(bomMulti, {
|
|
76
|
+
privateKey: rsaKeys.privateKey,
|
|
77
|
+
algorithm: "RS512",
|
|
78
|
+
mode: "signers",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.ok(
|
|
82
|
+
bomMulti.signature.algorithm,
|
|
83
|
+
"Initial signature block takes root format",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 2nd Pass: Second signer ONLY co-signs the root BOM.
|
|
87
|
+
signBom(bomMulti, {
|
|
88
|
+
privateKey: ecKeys.privateKey,
|
|
89
|
+
algorithm: "ES256",
|
|
90
|
+
mode: "signers",
|
|
91
|
+
signComponents: false,
|
|
92
|
+
signServices: false,
|
|
93
|
+
signAnnotations: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
assert.ok(
|
|
97
|
+
Array.isArray(bomMulti.signature.signers),
|
|
98
|
+
"Signature should be converted to signers array",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
assert.strictEqual(
|
|
102
|
+
bomMulti.signature.signers.length,
|
|
103
|
+
2,
|
|
104
|
+
"Should contain exactly two signers",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// RSA key signed EVERYTHING (root + components), so full deep verifyBom passes
|
|
108
|
+
assert.ok(verifyBom(bomMulti, rsaKeys.publicKey));
|
|
109
|
+
|
|
110
|
+
// EC key ONLY signed the root.
|
|
111
|
+
assert.ok(verifyNode(bomMulti, ecKeys.publicKey));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("Test Signature Chain Support (`chain`)", () => {
|
|
115
|
+
const bomChain = generateMockBom();
|
|
116
|
+
|
|
117
|
+
signBom(bomChain, {
|
|
118
|
+
privateKey: rsaKeys.privateKey,
|
|
119
|
+
algorithm: "RS512",
|
|
120
|
+
mode: "chain",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
signBom(bomChain, {
|
|
124
|
+
privateKey: ecKeys.privateKey,
|
|
125
|
+
algorithm: "ES256",
|
|
126
|
+
mode: "chain",
|
|
127
|
+
signComponents: false,
|
|
128
|
+
signServices: false,
|
|
129
|
+
signAnnotations: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.ok(
|
|
133
|
+
Array.isArray(bomChain.signature.chain),
|
|
134
|
+
"Signature should be converted to chain array",
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(bomChain.signature.chain.length, 2);
|
|
138
|
+
|
|
139
|
+
// RSA key signed everything, verifyBom works
|
|
140
|
+
assert.ok(verifyBom(bomChain, rsaKeys.publicKey));
|
|
141
|
+
|
|
142
|
+
// EC key only chained the root, verifyNode strictly checks the root
|
|
143
|
+
assert.ok(verifyNode(bomChain, ecKeys.publicKey));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("Test HMAC Symmetric Signature (HS256)", () => {
|
|
147
|
+
const symmetricKey = crypto.randomBytes(32).toString("hex");
|
|
148
|
+
const bomHmac = generateMockBom();
|
|
149
|
+
const signedHmac = signBom(bomHmac, {
|
|
150
|
+
privateKey: symmetricKey,
|
|
151
|
+
algorithm: "HS256",
|
|
152
|
+
});
|
|
153
|
+
assert.strictEqual(signedHmac.signature.algorithm, "HS256");
|
|
154
|
+
assert.ok(verifyBom(signedHmac, symmetricKey));
|
|
155
|
+
});
|
|
156
|
+
});
|