@indigoai-us/hq-cli 5.32.0 → 5.33.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/dist/commands/__fixtures__/make-tar.d.ts +46 -0
- package/dist/commands/__fixtures__/make-tar.js +105 -0
- package/dist/commands/pack-install.d.ts +187 -1
- package/dist/commands/pack-install.js +405 -16
- package/dist/commands/packs.js +9 -4
- package/dist/commands/publish.d.ts +186 -0
- package/dist/commands/publish.js +375 -0
- package/dist/commands/safe-extract.d.ts +154 -0
- package/dist/commands/safe-extract.js +347 -0
- package/dist/index.js +7 -2
- package/dist/lib/local-tree-diff.d.ts +21 -0
- package/dist/lib/local-tree-diff.js +18 -3
- package/dist/types.d.ts +22 -0
- package/dist/utils/vault-api.d.ts +11 -0
- package/dist/utils/vault-api.js +39 -2
- package/package.json +2 -2
- package/src/commands/__fixtures__/make-tar.ts +126 -0
- package/src/commands/artifact-verify.test.ts +177 -0
- package/src/commands/marketplace-install.test.ts +414 -0
- package/src/commands/marketplace-security.test.ts +646 -0
- package/src/commands/pack-install.test.ts +209 -1
- package/src/commands/pack-install.ts +617 -15
- package/src/commands/packs.ts +8 -1
- package/src/commands/publish.test.ts +538 -0
- package/src/commands/publish.ts +517 -0
- package/src/commands/safe-extract.test.ts +459 -0
- package/src/commands/safe-extract.ts +444 -0
- package/src/index.ts +6 -0
- package/src/lib/local-tree-diff.test.ts +19 -0
- package/src/lib/local-tree-diff.ts +17 -1
- package/src/types.ts +23 -0
- package/src/utils/vault-api.ts +41 -0
package/dist/utils/vault-api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="bdd04f0a-8d43-5072-916d-a801a2f25731")}catch(e){}}();
|
|
3
3
|
import { DEFAULT_VAULT_API_URL } from './cognito-session.js';
|
|
4
4
|
import { Sentry } from '../sentry.js';
|
|
5
5
|
export async function vaultApiFetch(opts) {
|
|
@@ -35,6 +35,43 @@ export async function vaultApiFetch(opts) {
|
|
|
35
35
|
}
|
|
36
36
|
return response;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Public (NONE-auth) GET against the vault API — no bearer token. The
|
|
40
|
+
* marketplace browse endpoints (`GET /v1/listings`, `GET /v1/listings/{id}`)
|
|
41
|
+
* from US-005 are public so a logged-out user can resolve + download an
|
|
42
|
+
* approved pack. Distinct from `vaultApiFetch`, which always attaches a
|
|
43
|
+
* bearer token.
|
|
44
|
+
*/
|
|
45
|
+
export async function vaultApiFetchPublic(opts) {
|
|
46
|
+
const url = new URL(opts.path, DEFAULT_VAULT_API_URL);
|
|
47
|
+
if (opts.query) {
|
|
48
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
49
|
+
url.searchParams.set(k, v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const safeUrl = url.search
|
|
53
|
+
? `${url.origin}${url.pathname}?<redacted>`
|
|
54
|
+
: `${url.origin}${url.pathname}`;
|
|
55
|
+
Sentry.addBreadcrumb({
|
|
56
|
+
category: 'http',
|
|
57
|
+
message: `GET ${opts.path}`,
|
|
58
|
+
level: 'info',
|
|
59
|
+
data: { url: safeUrl, method: 'GET' },
|
|
60
|
+
});
|
|
61
|
+
const response = await fetch(url.toString(), {
|
|
62
|
+
method: 'GET',
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
Sentry.addBreadcrumb({
|
|
67
|
+
category: 'http',
|
|
68
|
+
message: `GET ${opts.path} → ${response.status}`,
|
|
69
|
+
level: 'warning',
|
|
70
|
+
data: { url: safeUrl, status: response.status },
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
38
75
|
async function resolveCompanyUid(token, slug) {
|
|
39
76
|
const res = await vaultApiFetch({
|
|
40
77
|
token,
|
|
@@ -106,4 +143,4 @@ export async function getEntityUid(token, opts) {
|
|
|
106
143
|
return getCompanyUid(token, opts.companySlug);
|
|
107
144
|
}
|
|
108
145
|
//# sourceMappingURL=vault-api.js.map
|
|
109
|
-
//# debugId=
|
|
146
|
+
//# debugId=bdd04f0a-8d43-5072-916d-a801a2f25731
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.33.1",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"clean": "rm -rf dist"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@indigoai-us/hq-cloud": "~5.
|
|
18
|
+
"@indigoai-us/hq-cloud": "~5.47.0",
|
|
19
19
|
"@indigoai-us/hq-onboarding": "^0.1.0",
|
|
20
20
|
"@sentry/node": "^10.49.0",
|
|
21
21
|
"chalk": "^5.3.0",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic, platform-independent malicious-tar fixture builder.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS (cross-platform zip-slip coverage). The adversarial
|
|
5
|
+
* safe-extract / marketplace-security suites need archives that GENUINELY carry
|
|
6
|
+
* hostile entries — a `../` traversal name, an absolute path, an escaping
|
|
7
|
+
* symlink/hardlink. The obvious approach (shell out to the system `tar` to
|
|
8
|
+
* CREATE the archive) is NOT portable: GNU tar (Linux / CI / prod Lambdas)
|
|
9
|
+
* STRIPS the leading `../` and absolute `/` when it WRITES an archive, so the
|
|
10
|
+
* resulting "malicious" fixture contains a benign `evil-escape` entry and the
|
|
11
|
+
* guard has nothing to reject — the test passes on macOS bsdtar (which
|
|
12
|
+
* preserves them) but fails on Linux. The guard itself is correct; only the
|
|
13
|
+
* fixture construction was platform-dependent.
|
|
14
|
+
*
|
|
15
|
+
* Fix: WRITE THE TAR BYTES DIRECTLY here. A tar archive is just a sequence of
|
|
16
|
+
* 512-byte ustar headers, each optionally followed by the file's content padded
|
|
17
|
+
* to a 512-byte boundary, terminated by two zero blocks. By emitting the bytes
|
|
18
|
+
* ourselves we control the recorded entry names verbatim — `../evil-escape`,
|
|
19
|
+
* `/tmp/evil`, a symlink with linkname `../../etc/passwd` — identically on every
|
|
20
|
+
* platform, with no create-time stripping. The reading side (`tar -tvf`, which
|
|
21
|
+
* safeExtractTarball's pre-flight uses) does NOT strip anything; it faithfully
|
|
22
|
+
* lists whatever bytes we wrote, so the guard sees the real attack everywhere.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { gzipSync } from 'node:zlib';
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
|
|
28
|
+
const BLOCK = 512;
|
|
29
|
+
|
|
30
|
+
/** ustar typeflag values we use. */
|
|
31
|
+
export type TarType = 'file' | 'symlink' | 'hardlink';
|
|
32
|
+
|
|
33
|
+
const TYPEFLAG: Record<TarType, string> = {
|
|
34
|
+
file: '0', // regular file
|
|
35
|
+
symlink: '2', // symbolic link
|
|
36
|
+
hardlink: '1', // hard link
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface TarEntrySpec {
|
|
40
|
+
/** Recorded entry name — written VERBATIM (may contain `..`, be absolute…). */
|
|
41
|
+
name: string;
|
|
42
|
+
type?: TarType; // default 'file'
|
|
43
|
+
/** For sym/hardlinks: the recorded link target (also written verbatim). */
|
|
44
|
+
linkname?: string;
|
|
45
|
+
/** File content (regular files only). */
|
|
46
|
+
content?: string | Buffer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Write an ASCII string into a fixed-width field at `offset`, NUL-padded. */
|
|
50
|
+
function writeField(buf: Buffer, offset: number, width: number, value: string): void {
|
|
51
|
+
// Truncate defensively; ustar fields are fixed-width.
|
|
52
|
+
const s = value.slice(0, width);
|
|
53
|
+
buf.write(s, offset, 'ascii');
|
|
54
|
+
// Remaining bytes are already 0 from Buffer.alloc.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Write an octal numeric field: `width-1` octal digits, space-or-NUL terminated. */
|
|
58
|
+
function writeOctal(buf: Buffer, offset: number, width: number, value: number): void {
|
|
59
|
+
// Classic ustar numeric field: zero-padded octal in (width-1) chars + NUL.
|
|
60
|
+
const digits = width - 1;
|
|
61
|
+
const oct = value.toString(8).padStart(digits, '0').slice(-digits);
|
|
62
|
+
buf.write(oct, offset, 'ascii');
|
|
63
|
+
buf[offset + digits] = 0; // NUL terminator
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build one 512-byte ustar header (plus content blocks for regular files) for a
|
|
68
|
+
* single entry. The header checksum is computed exactly per spec: sum every
|
|
69
|
+
* header byte treating the 8 checksum bytes themselves as ASCII spaces, then
|
|
70
|
+
* write that sum as 6 octal digits + NUL + space.
|
|
71
|
+
*/
|
|
72
|
+
export function makeTarEntry(spec: TarEntrySpec): Buffer {
|
|
73
|
+
const type = spec.type ?? 'file';
|
|
74
|
+
const content =
|
|
75
|
+
type === 'file'
|
|
76
|
+
? Buffer.isBuffer(spec.content)
|
|
77
|
+
? spec.content
|
|
78
|
+
: Buffer.from(spec.content ?? '', 'utf-8')
|
|
79
|
+
: Buffer.alloc(0); // links carry no content payload
|
|
80
|
+
|
|
81
|
+
const header = Buffer.alloc(BLOCK); // zero-filled
|
|
82
|
+
|
|
83
|
+
writeField(header, 0, 100, spec.name); // name
|
|
84
|
+
writeOctal(header, 100, 8, 0o644); // mode
|
|
85
|
+
writeOctal(header, 108, 8, 0); // uid
|
|
86
|
+
writeOctal(header, 116, 8, 0); // gid
|
|
87
|
+
writeOctal(header, 124, 12, content.length); // size (0 for links)
|
|
88
|
+
writeOctal(header, 136, 12, 0); // mtime (deterministic: epoch)
|
|
89
|
+
// checksum field (148, 8) — filled below; start as spaces for the sum.
|
|
90
|
+
header.fill(' '.charCodeAt(0), 148, 156);
|
|
91
|
+
writeField(header, 156, 1, TYPEFLAG[type]); // typeflag
|
|
92
|
+
if (spec.linkname !== undefined) {
|
|
93
|
+
writeField(header, 157, 100, spec.linkname); // linkname (verbatim)
|
|
94
|
+
}
|
|
95
|
+
writeField(header, 257, 6, 'ustar'); // magic "ustar\0"
|
|
96
|
+
writeField(header, 263, 2, '00'); // version "00"
|
|
97
|
+
// uname/gname left empty; devmajor/devminor zero (already NUL).
|
|
98
|
+
|
|
99
|
+
// Header checksum: unsigned sum of all 512 bytes (with the checksum field as
|
|
100
|
+
// spaces, which we set above). Written as 6 octal digits, NUL, space.
|
|
101
|
+
let sum = 0;
|
|
102
|
+
for (let i = 0; i < BLOCK; i++) sum += header[i];
|
|
103
|
+
const cksum = sum.toString(8).padStart(6, '0').slice(-6);
|
|
104
|
+
header.write(cksum, 148, 'ascii');
|
|
105
|
+
header[154] = 0; // NUL
|
|
106
|
+
header[155] = ' '.charCodeAt(0); // space
|
|
107
|
+
|
|
108
|
+
if (type !== 'file' || content.length === 0) return header;
|
|
109
|
+
|
|
110
|
+
// Content padded up to a 512-byte boundary.
|
|
111
|
+
const pad = (BLOCK - (content.length % BLOCK)) % BLOCK;
|
|
112
|
+
return Buffer.concat([header, content, Buffer.alloc(pad)]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Concatenate entries and append the two zero blocks that end every tar. */
|
|
116
|
+
export function makeTar(entries: TarEntrySpec[]): Buffer {
|
|
117
|
+
const blocks = entries.map(makeTarEntry);
|
|
118
|
+
blocks.push(Buffer.alloc(BLOCK * 2)); // end-of-archive marker
|
|
119
|
+
return Buffer.concat(blocks);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Build the tar, gzip it, and write it to `outPath`. Returns `outPath`. */
|
|
123
|
+
export function writeMaliciousTarGz(outPath: string, entries: TarEntrySpec[]): string {
|
|
124
|
+
fs.writeFileSync(outPath, gzipSync(makeTar(entries)));
|
|
125
|
+
return outPath;
|
|
126
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplace artifact verification (US-021, INSTALL side) tests.
|
|
3
|
+
*
|
|
4
|
+
* Proves the install-time guarantee: the bytes we install are EXACTLY the bytes
|
|
5
|
+
* a moderator approved. Covers the two E2E behaviors required by the story:
|
|
6
|
+
* - an approved artifact whose S3 object is mutated → hash mismatch → REFUSE.
|
|
7
|
+
* - signature verification against the platform Ed25519 PUBLIC key.
|
|
8
|
+
*
|
|
9
|
+
* SHARED CONTRACT mirror — we reproduce the publish-side signing here with an
|
|
10
|
+
* ephemeral in-test keypair (no key material in the repo) to prove both sides
|
|
11
|
+
* agree byte-for-byte.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import {
|
|
16
|
+
generateKeyPairSync,
|
|
17
|
+
createHash,
|
|
18
|
+
sign as cryptoSign,
|
|
19
|
+
} from 'node:crypto';
|
|
20
|
+
import {
|
|
21
|
+
verifyArtifact,
|
|
22
|
+
computeArtifactHash,
|
|
23
|
+
ArtifactVerificationError,
|
|
24
|
+
ARTIFACT_HASH_ALG,
|
|
25
|
+
} from './pack-install.js';
|
|
26
|
+
|
|
27
|
+
// Ephemeral platform keypair (regenerated each run — never committed).
|
|
28
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
29
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
30
|
+
|
|
31
|
+
/** Reproduce the hq-pro publish-side: sha256 hex hash + Ed25519 over the hash. */
|
|
32
|
+
function publishSide(bytes: Buffer): { contentHash: string; signature: string } {
|
|
33
|
+
const contentHash = createHash('sha256').update(bytes).digest('hex');
|
|
34
|
+
const signature = cryptoSign(
|
|
35
|
+
null,
|
|
36
|
+
Buffer.from(contentHash, 'utf-8'),
|
|
37
|
+
privateKey,
|
|
38
|
+
).toString('base64');
|
|
39
|
+
return { contentHash, signature };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const APPROVED_BYTES = Buffer.from('the-approved-pack.tar.gz-bytes');
|
|
43
|
+
|
|
44
|
+
describe('computeArtifactHash', () => {
|
|
45
|
+
it('matches a lowercase-hex sha256 (shared contract with publish)', () => {
|
|
46
|
+
const expected = createHash('sha256').update(APPROVED_BYTES).digest('hex');
|
|
47
|
+
expect(computeArtifactHash(APPROVED_BYTES)).toBe(expected);
|
|
48
|
+
expect(ARTIFACT_HASH_ALG).toBe('sha256');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('verifyArtifact — integrity (hash)', () => {
|
|
53
|
+
it('passes when the downloaded bytes match the pinned hash + valid signature', () => {
|
|
54
|
+
const { contentHash, signature } = publishSide(APPROVED_BYTES);
|
|
55
|
+
expect(() =>
|
|
56
|
+
verifyArtifact({
|
|
57
|
+
tarballBytes: APPROVED_BYTES,
|
|
58
|
+
expectedHash: contentHash,
|
|
59
|
+
signature,
|
|
60
|
+
publicKey: publicKeyPem,
|
|
61
|
+
requireSignature: true,
|
|
62
|
+
}),
|
|
63
|
+
).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('E2E: a MUTATED S3 object → hash mismatch → REFUSES', () => {
|
|
67
|
+
// Publish/approval pinned the hash of APPROVED_BYTES…
|
|
68
|
+
const { contentHash, signature } = publishSide(APPROVED_BYTES);
|
|
69
|
+
// …but the bytes we actually downloaded were swapped by an attacker.
|
|
70
|
+
const mutatedBytes = Buffer.from('EVIL-swapped-pack.tar.gz-bytes');
|
|
71
|
+
|
|
72
|
+
expect(() =>
|
|
73
|
+
verifyArtifact({
|
|
74
|
+
tarballBytes: mutatedBytes,
|
|
75
|
+
expectedHash: contentHash,
|
|
76
|
+
signature,
|
|
77
|
+
publicKey: publicKeyPem,
|
|
78
|
+
}),
|
|
79
|
+
).toThrow(ArtifactVerificationError);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
verifyArtifact({
|
|
83
|
+
tarballBytes: mutatedBytes,
|
|
84
|
+
expectedHash: contentHash,
|
|
85
|
+
});
|
|
86
|
+
} catch (e) {
|
|
87
|
+
expect((e as Error).message).toMatch(/hash mismatch/i);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('refuses when the listing provides no valid hash', () => {
|
|
92
|
+
expect(() =>
|
|
93
|
+
verifyArtifact({ tarballBytes: APPROVED_BYTES, expectedHash: '' }),
|
|
94
|
+
).toThrow(/valid sha256 content hash/i);
|
|
95
|
+
expect(() =>
|
|
96
|
+
verifyArtifact({ tarballBytes: APPROVED_BYTES, expectedHash: 'not-a-hash' }),
|
|
97
|
+
).toThrow(ArtifactVerificationError);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('verifyArtifact — authenticity (signature)', () => {
|
|
102
|
+
it('refuses a hash-valid artifact whose signature was forged by a different key', () => {
|
|
103
|
+
const { contentHash } = publishSide(APPROVED_BYTES);
|
|
104
|
+
// Attacker signs the (correct) hash with their OWN key.
|
|
105
|
+
const { privateKey: evilKey } = generateKeyPairSync('ed25519');
|
|
106
|
+
const forged = cryptoSign(
|
|
107
|
+
null,
|
|
108
|
+
Buffer.from(contentHash, 'utf-8'),
|
|
109
|
+
evilKey,
|
|
110
|
+
).toString('base64');
|
|
111
|
+
|
|
112
|
+
expect(() =>
|
|
113
|
+
verifyArtifact({
|
|
114
|
+
tarballBytes: APPROVED_BYTES,
|
|
115
|
+
expectedHash: contentHash,
|
|
116
|
+
signature: forged,
|
|
117
|
+
publicKey: publicKeyPem, // platform key — won't validate the forgery
|
|
118
|
+
}),
|
|
119
|
+
).toThrow(/signature is invalid/i);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('refuses on a corrupt/garbage signature', () => {
|
|
123
|
+
const { contentHash } = publishSide(APPROVED_BYTES);
|
|
124
|
+
expect(() =>
|
|
125
|
+
verifyArtifact({
|
|
126
|
+
tarballBytes: APPROVED_BYTES,
|
|
127
|
+
expectedHash: contentHash,
|
|
128
|
+
signature: 'not-base64-or-a-real-sig!!!',
|
|
129
|
+
publicKey: publicKeyPem,
|
|
130
|
+
}),
|
|
131
|
+
).toThrow(ArtifactVerificationError);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('refuses on an invalid public key', () => {
|
|
135
|
+
const { contentHash, signature } = publishSide(APPROVED_BYTES);
|
|
136
|
+
expect(() =>
|
|
137
|
+
verifyArtifact({
|
|
138
|
+
tarballBytes: APPROVED_BYTES,
|
|
139
|
+
expectedHash: contentHash,
|
|
140
|
+
signature,
|
|
141
|
+
publicKey: 'not a real pem',
|
|
142
|
+
}),
|
|
143
|
+
).toThrow(/public key/i);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('verifyArtifact — deferred-key window', () => {
|
|
148
|
+
it('hash-verifies but skips signature when none provided (requireSignature=false)', () => {
|
|
149
|
+
const { contentHash } = publishSide(APPROVED_BYTES);
|
|
150
|
+
expect(() =>
|
|
151
|
+
verifyArtifact({ tarballBytes: APPROVED_BYTES, expectedHash: contentHash }),
|
|
152
|
+
).not.toThrow();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('refuses an unsigned artifact when requireSignature=true', () => {
|
|
156
|
+
const { contentHash } = publishSide(APPROVED_BYTES);
|
|
157
|
+
expect(() =>
|
|
158
|
+
verifyArtifact({
|
|
159
|
+
tarballBytes: APPROVED_BYTES,
|
|
160
|
+
expectedHash: contentHash,
|
|
161
|
+
requireSignature: true,
|
|
162
|
+
}),
|
|
163
|
+
).toThrow(/unsigned/i);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('refuses when a signature is present but no public key is available (requireSignature=true)', () => {
|
|
167
|
+
const { contentHash, signature } = publishSide(APPROVED_BYTES);
|
|
168
|
+
expect(() =>
|
|
169
|
+
verifyArtifact({
|
|
170
|
+
tarballBytes: APPROVED_BYTES,
|
|
171
|
+
expectedHash: contentHash,
|
|
172
|
+
signature,
|
|
173
|
+
requireSignature: true,
|
|
174
|
+
}),
|
|
175
|
+
).toThrow(/public key/i);
|
|
176
|
+
});
|
|
177
|
+
});
|