@eddacraft/anvil-policy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -0
- package/dist/bundle-manager.d.ts +183 -0
- package/dist/bundle-manager.d.ts.map +1 -0
- package/dist/bundle-manager.js +498 -0
- package/dist/bundle-verifier.d.ts +162 -0
- package/dist/bundle-verifier.d.ts.map +1 -0
- package/dist/bundle-verifier.js +401 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/opa-binary-manager.d.ts +76 -0
- package/dist/opa-binary-manager.d.ts.map +1 -0
- package/dist/opa-binary-manager.js +341 -0
- package/dist/opa-executor.d.ts +225 -0
- package/dist/opa-executor.d.ts.map +1 -0
- package/dist/opa-executor.js +427 -0
- package/dist/policy-loader.d.ts +90 -0
- package/dist/policy-loader.d.ts.map +1 -0
- package/dist/policy-loader.js +180 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/utils/debug.d.ts +9 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +44 -0
- package/package.json +33 -0
- package/project.json +8 -0
- package/src/bundle-manager.test.ts +588 -0
- package/src/bundle-manager.ts +710 -0
- package/src/bundle-verifier.test.ts +903 -0
- package/src/bundle-verifier.ts +568 -0
- package/src/index.ts +38 -0
- package/src/opa-binary-manager.test.ts +208 -0
- package/src/opa-binary-manager.ts +417 -0
- package/src/opa-executor.test.ts +1802 -0
- package/src/opa-executor.ts +681 -0
- package/src/policy-loader.test.ts +469 -0
- package/src/policy-loader.ts +262 -0
- package/src/types.ts +43 -0
- package/src/utils/debug.ts +54 -0
- package/tsconfig.json +12 -0
- package/tsconfig.lib.json +9 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for Bundle Verifier
|
|
3
|
+
*
|
|
4
|
+
* Tests signature verification for OPA policy bundles
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
BundleVerifier,
|
|
10
|
+
loadKeyFromFile,
|
|
11
|
+
type BundleVerifierConfig,
|
|
12
|
+
type PublicKeyConfig,
|
|
13
|
+
type SignatureManifest,
|
|
14
|
+
} from './bundle-verifier.js';
|
|
15
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { createHash, generateKeyPairSync, createSign } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
describe('BundleVerifier', () => {
|
|
21
|
+
let tempDir: string;
|
|
22
|
+
let bundleDir: string;
|
|
23
|
+
let verifier: BundleVerifier;
|
|
24
|
+
let rsaKeyPair: { publicKey: string; privateKey: string };
|
|
25
|
+
let ecKeyPair: { publicKey: string; privateKey: string };
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tempDir = join(tmpdir(), 'anvil-bundle-verifier-test', Math.random().toString(36));
|
|
29
|
+
bundleDir = join(tempDir, 'bundle');
|
|
30
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Generate test RSA key pair
|
|
33
|
+
rsaKeyPair = generateKeyPairSync('rsa', {
|
|
34
|
+
modulusLength: 2048,
|
|
35
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
36
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Generate test EC key pair
|
|
40
|
+
ecKeyPair = generateKeyPairSync('ec', {
|
|
41
|
+
namedCurve: 'P-256',
|
|
42
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
43
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const config: BundleVerifierConfig = {
|
|
47
|
+
keys: [
|
|
48
|
+
{
|
|
49
|
+
id: 'test-rsa-key',
|
|
50
|
+
algorithm: 'RS256',
|
|
51
|
+
key: rsaKeyPair.publicKey,
|
|
52
|
+
source: 'inline',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'test-ec-key',
|
|
56
|
+
algorithm: 'ES256',
|
|
57
|
+
key: ecKeyPair.publicKey,
|
|
58
|
+
source: 'inline',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
require_signature: false,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
verifier = new BundleVerifier(config);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
if (existsSync(tempDir)) {
|
|
69
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('initialization', () => {
|
|
74
|
+
it('should reject unsigned bundles when require_signature is true', async () => {
|
|
75
|
+
const strictVerifier = new BundleVerifier({
|
|
76
|
+
keys: [],
|
|
77
|
+
require_signature: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Create an unsigned bundle directory
|
|
81
|
+
const unsignedBundle = join(tempDir, 'unsigned-bundle');
|
|
82
|
+
mkdirSync(unsignedBundle, { recursive: true });
|
|
83
|
+
writeFileSync(join(unsignedBundle, 'policy.rego'), 'package test');
|
|
84
|
+
|
|
85
|
+
const result = await strictVerifier.verifyBundle(unsignedBundle);
|
|
86
|
+
expect(result.verified).toBe(false);
|
|
87
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should accept custom allowed algorithms and reject disallowed ones', async () => {
|
|
91
|
+
const restrictedVerifier = new BundleVerifier({
|
|
92
|
+
keys: [],
|
|
93
|
+
require_signature: true,
|
|
94
|
+
allowed_algorithms: ['RS256'],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Verifier should be created with restricted algorithms
|
|
98
|
+
// The restriction is tested via the actual verification flow in crypto tests below
|
|
99
|
+
expect(restrictedVerifier).toBeDefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('key management', () => {
|
|
104
|
+
it('should add keys and use them for verification', () => {
|
|
105
|
+
const newKey: PublicKeyConfig = {
|
|
106
|
+
id: 'new-key',
|
|
107
|
+
algorithm: 'RS256',
|
|
108
|
+
key: rsaKeyPair.publicKey,
|
|
109
|
+
source: 'inline',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
verifier.addKey(newKey);
|
|
113
|
+
|
|
114
|
+
// Verify key was added by confirming removeKey succeeds
|
|
115
|
+
const removed = verifier.removeKey('new-key');
|
|
116
|
+
expect(removed).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should remove keys', () => {
|
|
120
|
+
const removed = verifier.removeKey('test-rsa-key');
|
|
121
|
+
expect(removed).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return false when removing non-existent key', () => {
|
|
125
|
+
const removed = verifier.removeKey('non-existent-key');
|
|
126
|
+
expect(removed).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('file verification', () => {
|
|
131
|
+
it('should verify file with correct hash', async () => {
|
|
132
|
+
const filePath = join(bundleDir, 'policy.rego');
|
|
133
|
+
const content = 'package test\nallow = true';
|
|
134
|
+
writeFileSync(filePath, content);
|
|
135
|
+
|
|
136
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
137
|
+
const result = await verifier.verifyFile(filePath, hash);
|
|
138
|
+
|
|
139
|
+
expect(result).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should verify file with sha256 prefix', async () => {
|
|
143
|
+
const filePath = join(bundleDir, 'policy.rego');
|
|
144
|
+
const content = 'package test\nallow = true';
|
|
145
|
+
writeFileSync(filePath, content);
|
|
146
|
+
|
|
147
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
148
|
+
const result = await verifier.verifyFile(filePath, `sha256:${hash}`);
|
|
149
|
+
|
|
150
|
+
expect(result).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should reject file with incorrect hash', async () => {
|
|
154
|
+
const filePath = join(bundleDir, 'policy.rego');
|
|
155
|
+
writeFileSync(filePath, 'package test\nallow = true');
|
|
156
|
+
|
|
157
|
+
const result = await verifier.verifyFile(filePath, 'incorrect-hash-value'.padEnd(64, '0'));
|
|
158
|
+
|
|
159
|
+
expect(result).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return false for non-existent file', async () => {
|
|
163
|
+
const result = await verifier.verifyFile('/non/existent/file.rego', 'somehash');
|
|
164
|
+
|
|
165
|
+
expect(result).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('signature extraction', () => {
|
|
170
|
+
it('should extract valid signature manifest', async () => {
|
|
171
|
+
const manifest: SignatureManifest = {
|
|
172
|
+
signatures: [
|
|
173
|
+
{
|
|
174
|
+
files: [{ name: 'policy.rego', hash: 'sha256:abc123' }],
|
|
175
|
+
algorithm: 'RS256',
|
|
176
|
+
keyid: 'test-key',
|
|
177
|
+
signatures: ['base64sig'],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
183
|
+
|
|
184
|
+
const result = await verifier.extractSignatures(bundleDir);
|
|
185
|
+
|
|
186
|
+
expect(result).not.toBeNull();
|
|
187
|
+
expect(result?.signatures).toHaveLength(1);
|
|
188
|
+
expect(result?.signatures[0].keyid).toBe('test-key');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return null for missing signature file', async () => {
|
|
192
|
+
const result = await verifier.extractSignatures(bundleDir);
|
|
193
|
+
|
|
194
|
+
expect(result).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should return null for invalid JSON', async () => {
|
|
198
|
+
writeFileSync(join(bundleDir, '.signatures.json'), 'not valid json');
|
|
199
|
+
|
|
200
|
+
const result = await verifier.extractSignatures(bundleDir);
|
|
201
|
+
|
|
202
|
+
expect(result).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return null for invalid manifest structure', async () => {
|
|
206
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify({ invalid: true }));
|
|
207
|
+
|
|
208
|
+
const result = await verifier.extractSignatures(bundleDir);
|
|
209
|
+
|
|
210
|
+
expect(result).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should validate file entries in manifest', async () => {
|
|
214
|
+
const invalidManifest = {
|
|
215
|
+
signatures: [
|
|
216
|
+
{
|
|
217
|
+
files: [{ name: 'policy.rego' }], // Missing hash
|
|
218
|
+
algorithm: 'RS256',
|
|
219
|
+
keyid: 'test-key',
|
|
220
|
+
signatures: ['base64sig'],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(invalidManifest));
|
|
226
|
+
|
|
227
|
+
const result = await verifier.extractSignatures(bundleDir);
|
|
228
|
+
|
|
229
|
+
expect(result).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('bundle verification', () => {
|
|
234
|
+
it('should return verified=true for bundle without signature when not required', async () => {
|
|
235
|
+
// Create a policy file but no signatures
|
|
236
|
+
writeFileSync(join(bundleDir, 'policy.rego'), 'package test\nallow = true');
|
|
237
|
+
|
|
238
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
239
|
+
|
|
240
|
+
expect(result.verified).toBe(true);
|
|
241
|
+
expect(result.errors).toHaveLength(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should return verified=false for bundle without signature when required', async () => {
|
|
245
|
+
const strictVerifier = new BundleVerifier({
|
|
246
|
+
keys: [],
|
|
247
|
+
require_signature: true,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
writeFileSync(join(bundleDir, 'policy.rego'), 'package test\nallow = true');
|
|
251
|
+
|
|
252
|
+
const result = await strictVerifier.verifyBundle(bundleDir);
|
|
253
|
+
|
|
254
|
+
expect(result.verified).toBe(false);
|
|
255
|
+
expect(result.errors).toContain('No signature manifest found and signatures are required');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return error for non-existent bundle path', async () => {
|
|
259
|
+
const result = await verifier.verifyBundle('/non/existent/path');
|
|
260
|
+
|
|
261
|
+
expect(result.verified).toBe(false);
|
|
262
|
+
expect(result.errors[0]).toContain('Bundle path does not exist');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should reject bundle with unknown key ID', async () => {
|
|
266
|
+
const policyContent = 'package test\nallow = true';
|
|
267
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
268
|
+
|
|
269
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
270
|
+
const manifest: SignatureManifest = {
|
|
271
|
+
signatures: [
|
|
272
|
+
{
|
|
273
|
+
files: [{ name: 'policy.rego', hash: `sha256:${hash}` }],
|
|
274
|
+
algorithm: 'RS256',
|
|
275
|
+
keyid: 'unknown-key',
|
|
276
|
+
signatures: ['fakesig'],
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
282
|
+
|
|
283
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
284
|
+
|
|
285
|
+
expect(result.verified).toBe(false);
|
|
286
|
+
expect(result.errors).toContain('Unknown key ID: unknown-key');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should reject bundle with disallowed algorithm', async () => {
|
|
290
|
+
const restrictedVerifier = new BundleVerifier({
|
|
291
|
+
keys: [
|
|
292
|
+
{
|
|
293
|
+
id: 'test-rsa-key',
|
|
294
|
+
algorithm: 'RS256',
|
|
295
|
+
key: rsaKeyPair.publicKey,
|
|
296
|
+
source: 'inline',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
require_signature: true,
|
|
300
|
+
allowed_algorithms: ['ES256'], // Only allow ES256
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const policyContent = 'package test\nallow = true';
|
|
304
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
305
|
+
|
|
306
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
307
|
+
const manifest: SignatureManifest = {
|
|
308
|
+
signatures: [
|
|
309
|
+
{
|
|
310
|
+
files: [{ name: 'policy.rego', hash: `sha256:${hash}` }],
|
|
311
|
+
algorithm: 'RS256', // Not allowed
|
|
312
|
+
keyid: 'test-rsa-key',
|
|
313
|
+
signatures: ['fakesig'],
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
319
|
+
|
|
320
|
+
const result = await restrictedVerifier.verifyBundle(bundleDir);
|
|
321
|
+
|
|
322
|
+
expect(result.verified).toBe(false);
|
|
323
|
+
expect(result.errors).toContain('Algorithm not allowed: RS256');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should reject bundle with algorithm mismatch', async () => {
|
|
327
|
+
const policyContent = 'package test\nallow = true';
|
|
328
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
329
|
+
|
|
330
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
331
|
+
const manifest: SignatureManifest = {
|
|
332
|
+
signatures: [
|
|
333
|
+
{
|
|
334
|
+
files: [{ name: 'policy.rego', hash: `sha256:${hash}` }],
|
|
335
|
+
algorithm: 'ES256', // Mismatch: key is RS256
|
|
336
|
+
keyid: 'test-rsa-key',
|
|
337
|
+
signatures: ['fakesig'],
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
343
|
+
|
|
344
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
345
|
+
|
|
346
|
+
expect(result.verified).toBe(false);
|
|
347
|
+
expect(result.errors.some((e) => e.includes('Algorithm mismatch'))).toBe(true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('should reject bundle with incorrect file hash', async () => {
|
|
351
|
+
const policyContent = 'package test\nallow = true';
|
|
352
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
353
|
+
|
|
354
|
+
// Use wrong hash
|
|
355
|
+
const manifest: SignatureManifest = {
|
|
356
|
+
signatures: [
|
|
357
|
+
{
|
|
358
|
+
files: [{ name: 'policy.rego', hash: 'sha256:' + '0'.repeat(64) }],
|
|
359
|
+
algorithm: 'RS256',
|
|
360
|
+
keyid: 'test-rsa-key',
|
|
361
|
+
signatures: ['fakesig'],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
367
|
+
|
|
368
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
369
|
+
|
|
370
|
+
expect(result.verified).toBe(false);
|
|
371
|
+
expect(result.fileResults[0].verified).toBe(false);
|
|
372
|
+
expect(result.errors.some((e) => e.includes('File hash verification failed'))).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should verify bundle with valid RS256 signature', async () => {
|
|
376
|
+
const policyContent = 'package test\nallow = true';
|
|
377
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
378
|
+
|
|
379
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
380
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
381
|
+
|
|
382
|
+
// Create the signed data (canonical JSON of files array)
|
|
383
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
384
|
+
|
|
385
|
+
// Sign with RSA
|
|
386
|
+
const signer = createSign('RSA-SHA256');
|
|
387
|
+
signer.update(signedData);
|
|
388
|
+
const signature = signer.sign(rsaKeyPair.privateKey, 'base64');
|
|
389
|
+
|
|
390
|
+
const manifest: SignatureManifest = {
|
|
391
|
+
signatures: [
|
|
392
|
+
{
|
|
393
|
+
files,
|
|
394
|
+
algorithm: 'RS256',
|
|
395
|
+
keyid: 'test-rsa-key',
|
|
396
|
+
signatures: [signature],
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
402
|
+
|
|
403
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
404
|
+
|
|
405
|
+
expect(result.verified).toBe(true);
|
|
406
|
+
expect(result.keyId).toBe('test-rsa-key');
|
|
407
|
+
expect(result.errors).toHaveLength(0);
|
|
408
|
+
expect(result.fileResults[0].verified).toBe(true);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should verify bundle with valid ES256 signature', async () => {
|
|
412
|
+
const policyContent = 'package test\ndeny = false';
|
|
413
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
414
|
+
|
|
415
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
416
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
417
|
+
|
|
418
|
+
// Create the signed data
|
|
419
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
420
|
+
|
|
421
|
+
// Sign with EC
|
|
422
|
+
const signer = createSign('SHA256');
|
|
423
|
+
signer.update(signedData);
|
|
424
|
+
const signature = signer.sign(
|
|
425
|
+
{ key: ecKeyPair.privateKey, dsaEncoding: 'ieee-p1363' },
|
|
426
|
+
'base64'
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const manifest: SignatureManifest = {
|
|
430
|
+
signatures: [
|
|
431
|
+
{
|
|
432
|
+
files,
|
|
433
|
+
algorithm: 'ES256',
|
|
434
|
+
keyid: 'test-ec-key',
|
|
435
|
+
signatures: [signature],
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
441
|
+
|
|
442
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
443
|
+
|
|
444
|
+
expect(result.verified).toBe(true);
|
|
445
|
+
expect(result.keyId).toBe('test-ec-key');
|
|
446
|
+
expect(result.errors).toHaveLength(0);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should verify bundle with multiple files', async () => {
|
|
450
|
+
const policy1Content = 'package test.one\nallow = true';
|
|
451
|
+
const policy2Content = 'package test.two\ndeny = false';
|
|
452
|
+
|
|
453
|
+
writeFileSync(join(bundleDir, 'policy1.rego'), policy1Content);
|
|
454
|
+
writeFileSync(join(bundleDir, 'policy2.rego'), policy2Content);
|
|
455
|
+
|
|
456
|
+
const hash1 = createHash('sha256').update(policy1Content).digest('hex');
|
|
457
|
+
const hash2 = createHash('sha256').update(policy2Content).digest('hex');
|
|
458
|
+
|
|
459
|
+
const files = [
|
|
460
|
+
{ name: 'policy1.rego', hash: `sha256:${hash1}` },
|
|
461
|
+
{ name: 'policy2.rego', hash: `sha256:${hash2}` },
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
465
|
+
|
|
466
|
+
const signer = createSign('RSA-SHA256');
|
|
467
|
+
signer.update(signedData);
|
|
468
|
+
const signature = signer.sign(rsaKeyPair.privateKey, 'base64');
|
|
469
|
+
|
|
470
|
+
const manifest: SignatureManifest = {
|
|
471
|
+
signatures: [
|
|
472
|
+
{
|
|
473
|
+
files,
|
|
474
|
+
algorithm: 'RS256',
|
|
475
|
+
keyid: 'test-rsa-key',
|
|
476
|
+
signatures: [signature],
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
482
|
+
|
|
483
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
484
|
+
|
|
485
|
+
expect(result.verified).toBe(true);
|
|
486
|
+
expect(result.fileResults).toHaveLength(2);
|
|
487
|
+
expect(result.fileResults.every((r) => r.verified)).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should reject bundle with tampered file', async () => {
|
|
491
|
+
const originalContent = 'package test\nallow = true';
|
|
492
|
+
const hash = createHash('sha256').update(originalContent).digest('hex');
|
|
493
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
494
|
+
|
|
495
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
496
|
+
|
|
497
|
+
const signer = createSign('RSA-SHA256');
|
|
498
|
+
signer.update(signedData);
|
|
499
|
+
const signature = signer.sign(rsaKeyPair.privateKey, 'base64');
|
|
500
|
+
|
|
501
|
+
const manifest: SignatureManifest = {
|
|
502
|
+
signatures: [
|
|
503
|
+
{
|
|
504
|
+
files,
|
|
505
|
+
algorithm: 'RS256',
|
|
506
|
+
keyid: 'test-rsa-key',
|
|
507
|
+
signatures: [signature],
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// Write TAMPERED content
|
|
513
|
+
writeFileSync(join(bundleDir, 'policy.rego'), 'package test\nallow = false');
|
|
514
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
515
|
+
|
|
516
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
517
|
+
|
|
518
|
+
expect(result.verified).toBe(false);
|
|
519
|
+
expect(result.fileResults[0].verified).toBe(false);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('should reject bundle with invalid signature', async () => {
|
|
523
|
+
const policyContent = 'package test\nallow = true';
|
|
524
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
525
|
+
|
|
526
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
527
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
528
|
+
|
|
529
|
+
const manifest: SignatureManifest = {
|
|
530
|
+
signatures: [
|
|
531
|
+
{
|
|
532
|
+
files,
|
|
533
|
+
algorithm: 'RS256',
|
|
534
|
+
keyid: 'test-rsa-key',
|
|
535
|
+
signatures: ['aW52YWxpZC1zaWduYXR1cmU='], // Invalid signature
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
541
|
+
|
|
542
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
543
|
+
|
|
544
|
+
expect(result.verified).toBe(false);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should try multiple signature blocks until one succeeds', async () => {
|
|
548
|
+
// Generate a second RSA key pair
|
|
549
|
+
const rsaKeyPair2 = generateKeyPairSync('rsa', {
|
|
550
|
+
modulusLength: 2048,
|
|
551
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
552
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
verifier.addKey({
|
|
556
|
+
id: 'test-rsa-key-2',
|
|
557
|
+
algorithm: 'RS256',
|
|
558
|
+
key: rsaKeyPair2.publicKey,
|
|
559
|
+
source: 'inline',
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const policyContent = 'package test\nallow = true';
|
|
563
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
564
|
+
|
|
565
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
566
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
567
|
+
|
|
568
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
569
|
+
|
|
570
|
+
// Sign with second key
|
|
571
|
+
const signer = createSign('RSA-SHA256');
|
|
572
|
+
signer.update(signedData);
|
|
573
|
+
const signature = signer.sign(rsaKeyPair2.privateKey, 'base64');
|
|
574
|
+
|
|
575
|
+
const manifest: SignatureManifest = {
|
|
576
|
+
signatures: [
|
|
577
|
+
{
|
|
578
|
+
// First block with first key - invalid signature
|
|
579
|
+
files,
|
|
580
|
+
algorithm: 'RS256',
|
|
581
|
+
keyid: 'test-rsa-key',
|
|
582
|
+
signatures: ['aW52YWxpZA=='],
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
// Second block with second key - valid signature
|
|
586
|
+
files,
|
|
587
|
+
algorithm: 'RS256',
|
|
588
|
+
keyid: 'test-rsa-key-2',
|
|
589
|
+
signatures: [signature],
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
595
|
+
|
|
596
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
597
|
+
|
|
598
|
+
expect(result.verified).toBe(true);
|
|
599
|
+
expect(result.keyId).toBe('test-rsa-key-2');
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
describe('key sources', () => {
|
|
604
|
+
it('should load key from environment variable', async () => {
|
|
605
|
+
const envKeyName = 'ANVIL_VERIFY_KEY_' + Math.random().toString(36).slice(2);
|
|
606
|
+
process.env[envKeyName] = rsaKeyPair.publicKey;
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
const envVerifier = new BundleVerifier({
|
|
610
|
+
keys: [
|
|
611
|
+
{
|
|
612
|
+
id: 'env-key',
|
|
613
|
+
algorithm: 'RS256',
|
|
614
|
+
key: envKeyName,
|
|
615
|
+
source: 'env',
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
require_signature: true,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const policyContent = 'package test\nallow = true';
|
|
622
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
623
|
+
|
|
624
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
625
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
626
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
627
|
+
|
|
628
|
+
const signer = createSign('RSA-SHA256');
|
|
629
|
+
signer.update(signedData);
|
|
630
|
+
const signature = signer.sign(rsaKeyPair.privateKey, 'base64');
|
|
631
|
+
|
|
632
|
+
const manifest: SignatureManifest = {
|
|
633
|
+
signatures: [
|
|
634
|
+
{
|
|
635
|
+
files,
|
|
636
|
+
algorithm: 'RS256',
|
|
637
|
+
keyid: 'env-key',
|
|
638
|
+
signatures: [signature],
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
644
|
+
|
|
645
|
+
const result = await envVerifier.verifyBundle(bundleDir);
|
|
646
|
+
|
|
647
|
+
expect(result.verified).toBe(true);
|
|
648
|
+
} finally {
|
|
649
|
+
delete process.env[envKeyName];
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
it('should fail when environment variable is not set', async () => {
|
|
654
|
+
const envVerifier = new BundleVerifier({
|
|
655
|
+
keys: [
|
|
656
|
+
{
|
|
657
|
+
id: 'missing-env-key',
|
|
658
|
+
algorithm: 'RS256',
|
|
659
|
+
key: 'ANVIL_VERIFY_MISSING_' + Math.random().toString(36).slice(2),
|
|
660
|
+
source: 'env',
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
require_signature: true,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const policyContent = 'package test\nallow = true';
|
|
667
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
668
|
+
|
|
669
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
670
|
+
const manifest: SignatureManifest = {
|
|
671
|
+
signatures: [
|
|
672
|
+
{
|
|
673
|
+
files: [{ name: 'policy.rego', hash: `sha256:${hash}` }],
|
|
674
|
+
algorithm: 'RS256',
|
|
675
|
+
keyid: 'missing-env-key',
|
|
676
|
+
signatures: ['fakesig'],
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
682
|
+
|
|
683
|
+
const result = await envVerifier.verifyBundle(bundleDir);
|
|
684
|
+
|
|
685
|
+
expect(result.verified).toBe(false);
|
|
686
|
+
expect(result.errors.some((e) => e.includes('Environment variable not found'))).toBe(true);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe('env var allowlist', () => {
|
|
691
|
+
it('should reject env vars with disallowed prefixes', async () => {
|
|
692
|
+
const envVerifier = new BundleVerifier({
|
|
693
|
+
keys: [
|
|
694
|
+
{
|
|
695
|
+
id: 'blocked-key',
|
|
696
|
+
algorithm: 'RS256',
|
|
697
|
+
key: 'SECRET_KEY',
|
|
698
|
+
source: 'env',
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
require_signature: true,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
const policyContent = 'package test\nallow = true';
|
|
705
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
706
|
+
|
|
707
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
708
|
+
const manifest: SignatureManifest = {
|
|
709
|
+
signatures: [
|
|
710
|
+
{
|
|
711
|
+
files: [{ name: 'policy.rego', hash: `sha256:${hash}` }],
|
|
712
|
+
algorithm: 'RS256',
|
|
713
|
+
keyid: 'blocked-key',
|
|
714
|
+
signatures: ['fakesig'],
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
720
|
+
|
|
721
|
+
const result = await envVerifier.verifyBundle(bundleDir);
|
|
722
|
+
|
|
723
|
+
expect(result.verified).toBe(false);
|
|
724
|
+
expect(result.errors.some((e) => e.includes('not in allowlist'))).toBe(true);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('should reject AWS-style env vars', async () => {
|
|
728
|
+
const envVerifier = new BundleVerifier({
|
|
729
|
+
keys: [
|
|
730
|
+
{
|
|
731
|
+
id: 'aws-key',
|
|
732
|
+
algorithm: 'RS256',
|
|
733
|
+
key: 'AWS_SECRET_ACCESS_KEY',
|
|
734
|
+
source: 'env',
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
require_signature: true,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const policyContent = 'package test\nallow = true';
|
|
741
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
742
|
+
|
|
743
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
744
|
+
const manifest: SignatureManifest = {
|
|
745
|
+
signatures: [
|
|
746
|
+
{
|
|
747
|
+
files: [{ name: 'policy.rego', hash: `sha256:${hash}` }],
|
|
748
|
+
algorithm: 'RS256',
|
|
749
|
+
keyid: 'aws-key',
|
|
750
|
+
signatures: ['fakesig'],
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
756
|
+
|
|
757
|
+
const result = await envVerifier.verifyBundle(bundleDir);
|
|
758
|
+
|
|
759
|
+
expect(result.verified).toBe(false);
|
|
760
|
+
expect(result.errors.some((e) => e.includes('not in allowlist'))).toBe(true);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
it('should accept env vars with allowed prefixes', async () => {
|
|
764
|
+
const envKeyName = 'ANVIL_BUNDLE_PUBLIC_KEY_' + Math.random().toString(36).slice(2);
|
|
765
|
+
process.env[envKeyName] = rsaKeyPair.publicKey;
|
|
766
|
+
|
|
767
|
+
try {
|
|
768
|
+
const envVerifier = new BundleVerifier({
|
|
769
|
+
keys: [
|
|
770
|
+
{
|
|
771
|
+
id: 'allowed-key',
|
|
772
|
+
algorithm: 'RS256',
|
|
773
|
+
key: envKeyName,
|
|
774
|
+
source: 'env',
|
|
775
|
+
},
|
|
776
|
+
],
|
|
777
|
+
require_signature: true,
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
const policyContent = 'package test\nallow = true';
|
|
781
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
782
|
+
|
|
783
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
784
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
785
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
786
|
+
|
|
787
|
+
const signer = createSign('RSA-SHA256');
|
|
788
|
+
signer.update(signedData);
|
|
789
|
+
const signature = signer.sign(rsaKeyPair.privateKey, 'base64');
|
|
790
|
+
|
|
791
|
+
const manifest: SignatureManifest = {
|
|
792
|
+
signatures: [
|
|
793
|
+
{
|
|
794
|
+
files,
|
|
795
|
+
algorithm: 'RS256',
|
|
796
|
+
keyid: 'allowed-key',
|
|
797
|
+
signatures: [signature],
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
803
|
+
|
|
804
|
+
const result = await envVerifier.verifyBundle(bundleDir);
|
|
805
|
+
|
|
806
|
+
expect(result.verified).toBe(true);
|
|
807
|
+
} finally {
|
|
808
|
+
delete process.env[envKeyName];
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
describe('loadKeyFromFile', () => {
|
|
814
|
+
it('should load key from file', async () => {
|
|
815
|
+
const keyPath = join(tempDir, 'public-key.pem');
|
|
816
|
+
writeFileSync(keyPath, rsaKeyPair.publicKey);
|
|
817
|
+
|
|
818
|
+
const keyConfig = await loadKeyFromFile(keyPath, 'file-key', 'RS256');
|
|
819
|
+
|
|
820
|
+
expect(keyConfig.id).toBe('file-key');
|
|
821
|
+
expect(keyConfig.algorithm).toBe('RS256');
|
|
822
|
+
// loadKeyFromFile trims whitespace from the key
|
|
823
|
+
expect(keyConfig.key).toBe(rsaKeyPair.publicKey.trim());
|
|
824
|
+
expect(keyConfig.source).toBe('file');
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('should use loaded key for verification', async () => {
|
|
828
|
+
const keyPath = join(tempDir, 'public-key.pem');
|
|
829
|
+
writeFileSync(keyPath, rsaKeyPair.publicKey);
|
|
830
|
+
|
|
831
|
+
const keyConfig = await loadKeyFromFile(keyPath, 'loaded-key', 'RS256');
|
|
832
|
+
|
|
833
|
+
const fileVerifier = new BundleVerifier({
|
|
834
|
+
keys: [keyConfig],
|
|
835
|
+
require_signature: true,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const policyContent = 'package test\nallow = true';
|
|
839
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
840
|
+
|
|
841
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
842
|
+
const files = [{ name: 'policy.rego', hash: `sha256:${hash}` }];
|
|
843
|
+
const signedData = JSON.stringify([...files].sort((a, b) => a.name.localeCompare(b.name)));
|
|
844
|
+
|
|
845
|
+
const signer = createSign('RSA-SHA256');
|
|
846
|
+
signer.update(signedData);
|
|
847
|
+
const signature = signer.sign(rsaKeyPair.privateKey, 'base64');
|
|
848
|
+
|
|
849
|
+
const manifest: SignatureManifest = {
|
|
850
|
+
signatures: [
|
|
851
|
+
{
|
|
852
|
+
files,
|
|
853
|
+
algorithm: 'RS256',
|
|
854
|
+
keyid: 'loaded-key',
|
|
855
|
+
signatures: [signature],
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
861
|
+
|
|
862
|
+
const result = await fileVerifier.verifyBundle(bundleDir);
|
|
863
|
+
|
|
864
|
+
expect(result.verified).toBe(true);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
describe('error handling', () => {
|
|
869
|
+
it('should handle file read errors gracefully', async () => {
|
|
870
|
+
// Create a directory where a file is expected
|
|
871
|
+
const dirAsFile = join(bundleDir, 'policy.rego');
|
|
872
|
+
mkdirSync(dirAsFile, { recursive: true });
|
|
873
|
+
|
|
874
|
+
const result = await verifier.verifyFile(dirAsFile, 'somehash');
|
|
875
|
+
|
|
876
|
+
expect(result).toBe(false);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it('should provide detailed error messages', async () => {
|
|
880
|
+
const policyContent = 'package test\nallow = true';
|
|
881
|
+
writeFileSync(join(bundleDir, 'policy.rego'), policyContent);
|
|
882
|
+
|
|
883
|
+
const hash = createHash('sha256').update(policyContent).digest('hex');
|
|
884
|
+
const manifest: SignatureManifest = {
|
|
885
|
+
signatures: [
|
|
886
|
+
{
|
|
887
|
+
files: [{ name: 'policy.rego', hash: `sha256:${hash}` }],
|
|
888
|
+
algorithm: 'RS256',
|
|
889
|
+
keyid: 'test-rsa-key',
|
|
890
|
+
signatures: ['invalid-base64!@#'],
|
|
891
|
+
},
|
|
892
|
+
],
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
writeFileSync(join(bundleDir, '.signatures.json'), JSON.stringify(manifest));
|
|
896
|
+
|
|
897
|
+
const result = await verifier.verifyBundle(bundleDir);
|
|
898
|
+
|
|
899
|
+
expect(result.verified).toBe(false);
|
|
900
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
});
|