@pipobscure/bundle 0.0.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/HISTORY.md +1924 -0
- package/README.md +623 -0
- package/bundle.run +0 -0
- package/dist/api.d.ts +147 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +174 -0
- package/dist/api.js.map +1 -0
- package/dist/archive.d.ts +115 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +188 -0
- package/dist/archive.js.map +1 -0
- package/dist/audit.d.ts +78 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +119 -0
- package/dist/audit.js.map +1 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +555 -0
- package/dist/cli.js.map +1 -0
- package/dist/files.d.ts +53 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +118 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/launch.d.ts +97 -0
- package/dist/launch.d.ts.map +1 -0
- package/dist/launch.js +267 -0
- package/dist/launch.js.map +1 -0
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +19 -0
- package/dist/main.js.map +1 -0
- package/dist/manifest.d.ts +139 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +504 -0
- package/dist/manifest.js.map +1 -0
- package/dist/oidc.d.ts +40 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +320 -0
- package/dist/oidc.js.map +1 -0
- package/dist/preload.d.ts +14 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +38 -0
- package/dist/preload.js.map +1 -0
- package/dist/provider.d.ts +83 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +206 -0
- package/dist/provider.js.map +1 -0
- package/dist/record.d.ts +2 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +23 -0
- package/dist/record.js.map +1 -0
- package/dist/recorder.d.ts +64 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +111 -0
- package/dist/recorder.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +28 -0
- package/dist/register.js.map +1 -0
- package/dist/sea.d.ts +97 -0
- package/dist/sea.d.ts.map +1 -0
- package/dist/sea.js +220 -0
- package/dist/sea.js.map +1 -0
- package/dist/sigstore.d.ts +112 -0
- package/dist/sigstore.d.ts.map +1 -0
- package/dist/sigstore.js +385 -0
- package/dist/sigstore.js.map +1 -0
- package/dist/skill.d.ts +36 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +108 -0
- package/dist/skill.js.map +1 -0
- package/package.json +84 -0
- package/shell-base +2 -0
- package/skills/audit-bundle/SKILL.md +271 -0
- package/src/api.ts +293 -0
- package/src/archive.ts +312 -0
- package/src/audit.ts +206 -0
- package/src/cli.ts +575 -0
- package/src/files.ts +156 -0
- package/src/index.ts +114 -0
- package/src/launch.ts +336 -0
- package/src/main.ts +20 -0
- package/src/manifest.ts +615 -0
- package/src/oidc.ts +372 -0
- package/src/preload.ts +40 -0
- package/src/provider.ts +270 -0
- package/src/record.ts +25 -0
- package/src/recorder.ts +166 -0
- package/src/register.ts +30 -0
- package/src/sea.ts +341 -0
- package/src/sigstore.ts +492 -0
- package/src/skill.ts +132 -0
- package/src/types/node-vfs.d.ts +90 -0
- package/src/types/node-zip.d.ts +85 -0
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
import { type EmitResult, type Signer } from './archive.ts';
|
|
3
|
+
import { type VerificationResult, type VerifyOptions, type ArchiveSource, type ManifestFields } from './manifest.ts';
|
|
4
|
+
/** What a build wrote, and what it signed. */
|
|
5
|
+
export interface BuildResult extends EmitResult {
|
|
6
|
+
/** Where the archive was written, or null when it went to a stream. */
|
|
7
|
+
output: string | null;
|
|
8
|
+
/** Member names in the finished archive, excluding `AUTHORITY.PEM`. */
|
|
9
|
+
members: string[];
|
|
10
|
+
/** Size of the written file in bytes, when it went to a path. */
|
|
11
|
+
size?: number | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Where a build's bytes go: a path, a caller's stream, or stdout. */
|
|
14
|
+
interface Destination {
|
|
15
|
+
/** Where to write the archive. */
|
|
16
|
+
output?: string | undefined;
|
|
17
|
+
/** An open stream to write to instead. The caller closes it. */
|
|
18
|
+
stream?: Writable | undefined;
|
|
19
|
+
}
|
|
20
|
+
export interface CreateOptions extends Destination {
|
|
21
|
+
/** Base directory the file list is relative to (default: '.'). */
|
|
22
|
+
base?: string | undefined;
|
|
23
|
+
/** Member names, relative to `base`. */
|
|
24
|
+
files: string[];
|
|
25
|
+
/** A launcher or binary to prepend, making the result self-running. */
|
|
26
|
+
prefix?: string | undefined;
|
|
27
|
+
hashAlg?: string | undefined;
|
|
28
|
+
signAlg?: string | undefined;
|
|
29
|
+
/** Sign as it is built. Both must be given together. */
|
|
30
|
+
key?: Buffer | string | undefined;
|
|
31
|
+
chain?: string | undefined;
|
|
32
|
+
/** A two-phase signer, instead of `key`/`chain`. */
|
|
33
|
+
signer?: Signer | undefined;
|
|
34
|
+
}
|
|
35
|
+
export interface SignOptions extends Destination {
|
|
36
|
+
/** Path to the archive whose members are re-emitted and signed. */
|
|
37
|
+
source: string;
|
|
38
|
+
prefix?: string | undefined;
|
|
39
|
+
/** Make the output executable; implied by `prefix`. */
|
|
40
|
+
executable?: boolean | undefined;
|
|
41
|
+
hashAlg?: string | undefined;
|
|
42
|
+
signAlg?: string | undefined;
|
|
43
|
+
key?: Buffer | string | undefined;
|
|
44
|
+
chain?: string | undefined;
|
|
45
|
+
signer?: Signer | undefined;
|
|
46
|
+
}
|
|
47
|
+
export interface VerifyBundleOptions extends VerifyOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Extra trusted roots, as PEM text or as paths to PEM files — the
|
|
50
|
+
* convenience form of `extraRoots`, which takes PEM text only.
|
|
51
|
+
*/
|
|
52
|
+
roots?: string[] | undefined;
|
|
53
|
+
}
|
|
54
|
+
export interface RunOptions {
|
|
55
|
+
/** Extra trusted roots, as PEM text or paths to PEM files. */
|
|
56
|
+
roots?: string[] | undefined;
|
|
57
|
+
identity?: string | undefined;
|
|
58
|
+
issuer?: string | undefined;
|
|
59
|
+
/** Run an archive whose signature is good but whose chain is unanchored. */
|
|
60
|
+
allowUntrusted?: boolean | undefined;
|
|
61
|
+
/** Arguments handed to the application inside the archive. */
|
|
62
|
+
args?: string[] | undefined;
|
|
63
|
+
/** Extra environment for the child, merged over `process.env`. */
|
|
64
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
65
|
+
/** How the child's stdio is wired (default: 'inherit'). */
|
|
66
|
+
stdio?: 'inherit' | 'pipe' | undefined;
|
|
67
|
+
}
|
|
68
|
+
export interface RunResult {
|
|
69
|
+
/** The child's exit status, or null when it was killed by a signal. */
|
|
70
|
+
status: number | null;
|
|
71
|
+
signal: NodeJS.Signals | null;
|
|
72
|
+
/** Captured only when `stdio` was 'pipe'. */
|
|
73
|
+
stdout?: string | undefined;
|
|
74
|
+
stderr?: string | undefined;
|
|
75
|
+
}
|
|
76
|
+
/** What an archive says about itself, with no trust decision attached. */
|
|
77
|
+
export interface Inspection {
|
|
78
|
+
/** Member names, excluding `AUTHORITY.PEM`. */
|
|
79
|
+
members: string[];
|
|
80
|
+
/** Whether the archive carries a signature marker at all. */
|
|
81
|
+
signed: boolean;
|
|
82
|
+
/** The whole-file hash the marker records, hex. */
|
|
83
|
+
hash?: string | undefined;
|
|
84
|
+
/** Names of the unsigned attributes carried beside the signature. */
|
|
85
|
+
fields: string[];
|
|
86
|
+
/** The manifest's declared algorithms and certificate chain. */
|
|
87
|
+
manifest?: ManifestFields | undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build an archive from a base directory and a list of files, optionally
|
|
91
|
+
* signing it and optionally prepending a launcher or binary.
|
|
92
|
+
*/
|
|
93
|
+
export declare function createBundle(options: CreateOptions): Promise<BuildResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Sign an existing archive into a new file. The input is never modified: its
|
|
96
|
+
* members are read out, laid down again behind whatever prefix was asked for,
|
|
97
|
+
* and the finished bytes are hashed and signed as a whole. One unsigned archive
|
|
98
|
+
* therefore yields every shape — a `#!` launcher, a self-contained binary, or a
|
|
99
|
+
* plain mountable archive — each correctly offset and each signed over itself.
|
|
100
|
+
*/
|
|
101
|
+
export declare function signBundle(options: SignOptions): Promise<BuildResult>;
|
|
102
|
+
/** A signer backed by a private key and certificate chain read from disk. */
|
|
103
|
+
export declare function fileSigner({ key, chain, signAlg }: {
|
|
104
|
+
key: string;
|
|
105
|
+
chain: string;
|
|
106
|
+
signAlg?: string | undefined;
|
|
107
|
+
}): Signer;
|
|
108
|
+
/**
|
|
109
|
+
* Verify an archive: recompute the whole-file hash, check the signature over it
|
|
110
|
+
* against the leaf certificate, check every member's own digest, and decide
|
|
111
|
+
* whether the certificate chain means anything to us.
|
|
112
|
+
*/
|
|
113
|
+
export declare function verifyBundle(source: ArchiveSource, options?: VerifyBundleOptions): Promise<VerificationResult>;
|
|
114
|
+
/** The synchronous form, for callers on a path that cannot await — a mount. */
|
|
115
|
+
export declare function verifyBundleSync(source: ArchiveSource, options?: VerifyBundleOptions): VerificationResult;
|
|
116
|
+
/**
|
|
117
|
+
* What an archive claims about itself — its members, whether it is signed at
|
|
118
|
+
* all, and what its manifest declares. This is the cheap "what am I looking at"
|
|
119
|
+
* call; `verifyBundle` is the expensive one that answers whether any of it is
|
|
120
|
+
* true, and nothing here should be believed until it has run.
|
|
121
|
+
*/
|
|
122
|
+
export declare function inspectBundle(source: string): Inspection;
|
|
123
|
+
/**
|
|
124
|
+
* Mount a signed archive and run the application inside it, in a child process
|
|
125
|
+
* with the verifying provider preloaded — so the archive is checked and mounted
|
|
126
|
+
* by the child's own bootstrap, and what runs is what was verified.
|
|
127
|
+
*
|
|
128
|
+
* Verification happens here too, before the child is spawned. That buys nothing
|
|
129
|
+
* the child does not already enforce; it buys a legible refusal instead of an
|
|
130
|
+
* uncaught error thrown from inside node's startup.
|
|
131
|
+
*/
|
|
132
|
+
export declare function runBundle(archive: string, options?: RunOptions): RunResult;
|
|
133
|
+
/**
|
|
134
|
+
* The node arguments that mount `archive` as the filesystem and run the program
|
|
135
|
+
* inside it, with this package's verifying provider preloaded. Anything after
|
|
136
|
+
* these is the application's own argv — the trailing `--` is what makes that
|
|
137
|
+
* true, since without it node claims any argument that looks like one of its
|
|
138
|
+
* own flags and the application never sees it.
|
|
139
|
+
*/
|
|
140
|
+
export declare function mountArgv(archive: string): string[];
|
|
141
|
+
/**
|
|
142
|
+
* The absolute path of the preload that registers the verifying provider, as a
|
|
143
|
+
* real path `node -r` can resolve.
|
|
144
|
+
*/
|
|
145
|
+
export declare function registerPath(): string;
|
|
146
|
+
export {};
|
|
147
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as FS from 'node:fs';
|
|
2
|
+
import * as PATH from 'node:path';
|
|
3
|
+
import * as ZLIB from 'node:zlib';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { bundle, rebundle, keySigner, members } from './archive.js';
|
|
6
|
+
import { verify, verifySync, signatureOf, parseManifest, AUTHORITY, } from './manifest.js';
|
|
7
|
+
/**
|
|
8
|
+
* Build an archive from a base directory and a list of files, optionally
|
|
9
|
+
* signing it and optionally prepending a launcher or binary.
|
|
10
|
+
*/
|
|
11
|
+
export async function createBundle(options) {
|
|
12
|
+
const { base = '.', files, prefix, hashAlg, signAlg, key, chain, signer } = options;
|
|
13
|
+
if (!files.length)
|
|
14
|
+
throw new Error('create: the file list is empty');
|
|
15
|
+
if (!signer && Boolean(key) !== Boolean(chain))
|
|
16
|
+
throw new Error('create: key and chain must be given together');
|
|
17
|
+
return await produce(options, Boolean(prefix), (out) => bundle({
|
|
18
|
+
base, files, prefix, hashAlg, signAlg, key, chain, signer, out,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sign an existing archive into a new file. The input is never modified: its
|
|
23
|
+
* members are read out, laid down again behind whatever prefix was asked for,
|
|
24
|
+
* and the finished bytes are hashed and signed as a whole. One unsigned archive
|
|
25
|
+
* therefore yields every shape — a `#!` launcher, a self-contained binary, or a
|
|
26
|
+
* plain mountable archive — each correctly offset and each signed over itself.
|
|
27
|
+
*/
|
|
28
|
+
export async function signBundle(options) {
|
|
29
|
+
const { source, output, prefix, executable, hashAlg, signAlg, key, chain, signer } = options;
|
|
30
|
+
if (!source)
|
|
31
|
+
throw new Error('sign: an archive path is required');
|
|
32
|
+
if (!signer && Boolean(key) !== Boolean(chain))
|
|
33
|
+
throw new Error('sign: key and chain must be given together');
|
|
34
|
+
if (output && PATH.resolve(output) === PATH.resolve(source)) {
|
|
35
|
+
throw new Error('sign: the output must differ from the input archive');
|
|
36
|
+
}
|
|
37
|
+
return await produce(options, Boolean(prefix || executable), (out) => rebundle({
|
|
38
|
+
source, prefix, hashAlg, signAlg, key, chain, signer, out,
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
/** A signer backed by a private key and certificate chain read from disk. */
|
|
42
|
+
export function fileSigner({ key, chain, signAlg = 'sha256' }) {
|
|
43
|
+
return keySigner({ key: FS.readFileSync(key), chain: FS.readFileSync(chain, 'utf-8'), signAlg });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verify an archive: recompute the whole-file hash, check the signature over it
|
|
47
|
+
* against the leaf certificate, check every member's own digest, and decide
|
|
48
|
+
* whether the certificate chain means anything to us.
|
|
49
|
+
*/
|
|
50
|
+
export async function verifyBundle(source, options) {
|
|
51
|
+
return verify(source, withRoots(options));
|
|
52
|
+
}
|
|
53
|
+
/** The synchronous form, for callers on a path that cannot await — a mount. */
|
|
54
|
+
export function verifyBundleSync(source, options) {
|
|
55
|
+
return verifySync(source, withRoots(options));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What an archive claims about itself — its members, whether it is signed at
|
|
59
|
+
* all, and what its manifest declares. This is the cheap "what am I looking at"
|
|
60
|
+
* call; `verifyBundle` is the expensive one that answers whether any of it is
|
|
61
|
+
* true, and nothing here should be believed until it has run.
|
|
62
|
+
*/
|
|
63
|
+
export function inspectBundle(source) {
|
|
64
|
+
const marker = signatureOf(source);
|
|
65
|
+
const names = members(source);
|
|
66
|
+
let manifest;
|
|
67
|
+
const zip = ZLIB.ZipFile.openSync(PATH.resolve(source));
|
|
68
|
+
try {
|
|
69
|
+
if (zip.has(AUTHORITY))
|
|
70
|
+
manifest = parseManifest(zip.getSync(AUTHORITY).contentSync());
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
zip.closeSync();
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
members: names,
|
|
77
|
+
signed: marker !== null,
|
|
78
|
+
hash: marker?.hash,
|
|
79
|
+
fields: marker ? [...marker.fields.keys()] : [],
|
|
80
|
+
manifest,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Mount a signed archive and run the application inside it, in a child process
|
|
85
|
+
* with the verifying provider preloaded — so the archive is checked and mounted
|
|
86
|
+
* by the child's own bootstrap, and what runs is what was verified.
|
|
87
|
+
*
|
|
88
|
+
* Verification happens here too, before the child is spawned. That buys nothing
|
|
89
|
+
* the child does not already enforce; it buys a legible refusal instead of an
|
|
90
|
+
* uncaught error thrown from inside node's startup.
|
|
91
|
+
*/
|
|
92
|
+
export function runBundle(archive, options = {}) {
|
|
93
|
+
const roots = options.roots ?? [];
|
|
94
|
+
const res = verifyBundleSync(archive, {
|
|
95
|
+
roots, deep: false, identity: options.identity, issuer: options.issuer,
|
|
96
|
+
});
|
|
97
|
+
const acceptable = res.state === 'valid' || (Boolean(options.allowUntrusted) && res.state === 'valid-untrusted');
|
|
98
|
+
if (!acceptable) {
|
|
99
|
+
throw Object.assign(new Error(`refusing to run '${archive}': ${res.state} — ${res.reason}`), { code: 'ERR_BUNDLE_UNTRUSTED', state: res.state });
|
|
100
|
+
}
|
|
101
|
+
const env = { ...process.env, ...options.env };
|
|
102
|
+
if (roots.length)
|
|
103
|
+
env['BUNDLE_ROOTS'] = roots.join(PATH.delimiter);
|
|
104
|
+
if (options.identity)
|
|
105
|
+
env['BUNDLE_IDENTITY'] = options.identity;
|
|
106
|
+
if (options.issuer)
|
|
107
|
+
env['BUNDLE_ISSUER'] = options.issuer;
|
|
108
|
+
if (options.allowUntrusted)
|
|
109
|
+
env['BUNDLE_ALLOW_UNTRUSTED'] = '1';
|
|
110
|
+
const child = spawnSync(process.execPath, [...mountArgv(archive), ...(options.args ?? [])], { stdio: options.stdio ?? 'inherit', env, encoding: 'utf-8' });
|
|
111
|
+
if (child.error)
|
|
112
|
+
throw child.error;
|
|
113
|
+
return { status: child.status, signal: child.signal, stdout: child.stdout, stderr: child.stderr };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The node arguments that mount `archive` as the filesystem and run the program
|
|
117
|
+
* inside it, with this package's verifying provider preloaded. Anything after
|
|
118
|
+
* these is the application's own argv — the trailing `--` is what makes that
|
|
119
|
+
* true, since without it node claims any argument that looks like one of its
|
|
120
|
+
* own flags and the application never sees it.
|
|
121
|
+
*/
|
|
122
|
+
export function mountArgv(archive) {
|
|
123
|
+
return [
|
|
124
|
+
'--no-warnings', '--experimental-vfs',
|
|
125
|
+
'-r', registerPath(),
|
|
126
|
+
'--vfs-load', archive,
|
|
127
|
+
'--',
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The absolute path of the preload that registers the verifying provider, as a
|
|
132
|
+
* real path `node -r` can resolve.
|
|
133
|
+
*/
|
|
134
|
+
export function registerPath() {
|
|
135
|
+
const ext = import.meta.filename.endsWith('.ts') ? '.ts' : '.js';
|
|
136
|
+
return PATH.join(PATH.dirname(import.meta.filename), `register${ext}`);
|
|
137
|
+
}
|
|
138
|
+
// `roots` may name PEM files or carry PEM text; `verifySync` wants text.
|
|
139
|
+
function withRoots(options) {
|
|
140
|
+
if (!options)
|
|
141
|
+
return {};
|
|
142
|
+
const { roots, ...rest } = options;
|
|
143
|
+
if (!roots?.length)
|
|
144
|
+
return rest;
|
|
145
|
+
const loaded = roots.map((root) => (root.includes('-----BEGIN') ? root : FS.readFileSync(root, 'utf-8')));
|
|
146
|
+
return { ...rest, extraRoots: [...(rest.extraRoots ?? []), ...loaded] };
|
|
147
|
+
}
|
|
148
|
+
// Open the destination, run the build into it, wait for the bytes to land, and
|
|
149
|
+
// report what was written. A stream the caller supplied is left open — its
|
|
150
|
+
// lifetime is theirs — while one opened here is closed and waited on. stdout is
|
|
151
|
+
// ended (so a redirect sees EOF) but not awaited for 'finish', which never
|
|
152
|
+
// fires for a TTY or a pipe.
|
|
153
|
+
async function produce({ output, stream }, executable, build) {
|
|
154
|
+
const out = stream ?? (output ? FS.createWriteStream(output) : process.stdout);
|
|
155
|
+
const res = await build(out);
|
|
156
|
+
if (!stream)
|
|
157
|
+
await close(out);
|
|
158
|
+
if (output && executable)
|
|
159
|
+
FS.chmodSync(output, 0o755);
|
|
160
|
+
return {
|
|
161
|
+
...res,
|
|
162
|
+
output: output ?? null,
|
|
163
|
+
members: output ? members(output) : [],
|
|
164
|
+
size: output ? FS.statSync(output).size : undefined,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function close(out) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
if (out === process.stdout)
|
|
170
|
+
return void out.end(() => resolve());
|
|
171
|
+
out.on('error', reject).on('finish', () => resolve()).end();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as ZLIB from 'node:zlib';
|
|
2
|
+
import * as CRYPTO from 'node:crypto';
|
|
3
|
+
import { type Writable } from 'node:stream';
|
|
4
|
+
/** One file about to become an archive member. */
|
|
5
|
+
export interface Member {
|
|
6
|
+
/** The member's name inside the archive; a `/`-separated relative path. */
|
|
7
|
+
name: string;
|
|
8
|
+
data: Buffer;
|
|
9
|
+
mode?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** What a signer hands back once the finished hash exists. */
|
|
12
|
+
export interface Signature {
|
|
13
|
+
signature: Buffer | Uint8Array;
|
|
14
|
+
/**
|
|
15
|
+
* Unsigned attributes to record beside the signature in the EOCD comment —
|
|
16
|
+
* anything obtained *after* signing, which therefore cannot be inside what
|
|
17
|
+
* the signature covers.
|
|
18
|
+
*/
|
|
19
|
+
fields?: Record<string, string | undefined> | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The two-phase signing interface. `chain` is known up front (it goes into
|
|
23
|
+
* `AUTHORITY.PEM`, inside the hashed region); `sign()` is called afterwards
|
|
24
|
+
* with the hash of the finished bytes.
|
|
25
|
+
*/
|
|
26
|
+
export interface Signer {
|
|
27
|
+
kind?: string | undefined;
|
|
28
|
+
/** Full PEM certificate chain, leaf first. */
|
|
29
|
+
chain: string;
|
|
30
|
+
/** Digest the signature over the whole-file hash uses. */
|
|
31
|
+
signAlg: string;
|
|
32
|
+
sign(digest: Buffer): Promise<Signature>;
|
|
33
|
+
}
|
|
34
|
+
/** What `emit()` reports once the file has been fully written. */
|
|
35
|
+
export interface EmitResult {
|
|
36
|
+
/** The whole-file hash, hex — null for an unsigned archive. */
|
|
37
|
+
hash: string | null;
|
|
38
|
+
signed: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface BundleOptions {
|
|
41
|
+
/** Base directory the file list is relative to. */
|
|
42
|
+
base: string;
|
|
43
|
+
/** Member names, relative to `base`. */
|
|
44
|
+
files: string[];
|
|
45
|
+
/** A launcher or binary to prepend, making the result self-running. */
|
|
46
|
+
prefix?: string | undefined;
|
|
47
|
+
/** Digest for the whole-file hash and member digests (default: 'sha256'). */
|
|
48
|
+
hashAlg?: string | undefined;
|
|
49
|
+
/** Digest the signature uses (default: 'sha256'). */
|
|
50
|
+
signAlg?: string | undefined;
|
|
51
|
+
/** Shorthand for `signer: keySigner({ key, chain, signAlg })`. */
|
|
52
|
+
key?: Buffer | string | CRYPTO.KeyObject | undefined;
|
|
53
|
+
chain?: string | undefined;
|
|
54
|
+
signer?: Signer | undefined;
|
|
55
|
+
out: Writable;
|
|
56
|
+
}
|
|
57
|
+
export interface RebundleOptions {
|
|
58
|
+
/** Path to the archive whose members are re-emitted. */
|
|
59
|
+
source: string;
|
|
60
|
+
prefix?: string | undefined;
|
|
61
|
+
hashAlg?: string | undefined;
|
|
62
|
+
signAlg?: string | undefined;
|
|
63
|
+
key?: Buffer | string | CRYPTO.KeyObject | undefined;
|
|
64
|
+
chain?: string | undefined;
|
|
65
|
+
signer?: Signer | undefined;
|
|
66
|
+
out: Writable;
|
|
67
|
+
}
|
|
68
|
+
export declare function fromDirectory(base: string, files: string[]): AsyncGenerator<Member>;
|
|
69
|
+
export declare function fromArchive(zip: ZLIB.ZipFile): AsyncGenerator<Member>;
|
|
70
|
+
/**
|
|
71
|
+
* A Readable of the ZIP archive over `members`. Its members carry per-file
|
|
72
|
+
* digests and its AUTHORITY.PEM manifest declares the algorithms and chain, but
|
|
73
|
+
* the archive itself is left with an empty EOCD comment: the whole-file hash
|
|
74
|
+
* and its signature are a property of the finished file and are applied by
|
|
75
|
+
* `emit()`. `baseOffset` seeds the archive's internal offsets for when it is
|
|
76
|
+
* appended after a prefix.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createArchive({ members, base, files, hashAlg, signAlg, chain, baseOffset }: {
|
|
79
|
+
members?: AsyncIterable<Member> | Iterable<Member> | undefined;
|
|
80
|
+
base?: string | undefined;
|
|
81
|
+
files?: string[] | undefined;
|
|
82
|
+
hashAlg?: string | undefined;
|
|
83
|
+
signAlg?: string | undefined;
|
|
84
|
+
chain?: string | undefined;
|
|
85
|
+
baseOffset?: number | undefined;
|
|
86
|
+
}): import("node:stream").Readable;
|
|
87
|
+
/**
|
|
88
|
+
* A signer backed by a private key and a certificate chain already on disk —
|
|
89
|
+
* the offline-CA path, and the shape `sigstore.ts` implements too.
|
|
90
|
+
*/
|
|
91
|
+
export declare function keySigner({ key, chain, signAlg }: {
|
|
92
|
+
key: Buffer | string | CRYPTO.KeyObject;
|
|
93
|
+
chain: Buffer | string;
|
|
94
|
+
signAlg?: string | undefined;
|
|
95
|
+
}): Signer;
|
|
96
|
+
/**
|
|
97
|
+
* Build an archive from files on disk. `key`/`chain` are accepted as a
|
|
98
|
+
* shorthand for `signer: keySigner({ key, chain, signAlg })`, so the
|
|
99
|
+
* create-and-sign-in-one-step path stays available.
|
|
100
|
+
*/
|
|
101
|
+
export declare function bundle({ base, files, prefix, hashAlg, signAlg, key, chain, signer, out }: BundleOptions): Promise<EmitResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Re-emit an existing archive: same members, new prefix, new signature. This is
|
|
104
|
+
* what `bundle sign` runs. `source` is a path to an archive (signed or not,
|
|
105
|
+
* prefixed or not) — its members are read out, its old AUTHORITY.PEM is
|
|
106
|
+
* dropped, and everything is laid down again at the offsets the new prefix
|
|
107
|
+
* implies before the result is hashed and signed as a whole.
|
|
108
|
+
*/
|
|
109
|
+
export declare function rebundle({ source, prefix, hashAlg, signAlg, key, chain, signer, out }: RebundleOptions): Promise<EmitResult>;
|
|
110
|
+
/**
|
|
111
|
+
* The member names an archive holds, in order, excluding AUTHORITY.PEM — for
|
|
112
|
+
* reporting what is about to be re-signed without reading every member's bytes.
|
|
113
|
+
*/
|
|
114
|
+
export declare function members(source: string): string[];
|
|
115
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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"}
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as ZLIB from 'node:zlib';
|
|
2
|
+
import * as PATH from 'node:path';
|
|
3
|
+
import * as FS from 'node:fs';
|
|
4
|
+
import * as CRYPTO from 'node:crypto';
|
|
5
|
+
import { Transform } from 'node:stream';
|
|
6
|
+
import { buildManifest, formatSignature, AUTHORITY } from './manifest.js';
|
|
7
|
+
// Members, as `{ name, data, mode }`. Two sources: a directory plus a file
|
|
8
|
+
// list, or an existing archive.
|
|
9
|
+
//
|
|
10
|
+
// `AUTHORITY.PEM` is never carried across from a source archive — it describes
|
|
11
|
+
// the signing of the archive it came from, and a re-emitted archive gets a
|
|
12
|
+
// fresh one.
|
|
13
|
+
export async function* fromDirectory(base, files) {
|
|
14
|
+
for (const name of files) {
|
|
15
|
+
yield { name, data: FS.readFileSync(PATH.resolve(base, name)), mode: 0o444 };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function* fromArchive(zip) {
|
|
19
|
+
for (const [name, entry] of zip.entriesSync()) {
|
|
20
|
+
if (name === AUTHORITY || entry.isDirectory)
|
|
21
|
+
continue;
|
|
22
|
+
yield { name, data: entry.contentSync(), mode: entry.mode || 0o444 };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Yields a ZipEntry per member — each stamped, in its entry comment, with the
|
|
26
|
+
// hex digest of its own content — then a final `AUTHORITY.PEM` manifest entry
|
|
27
|
+
// declaring the algorithms and (when signing) carrying the certificate chain.
|
|
28
|
+
// Members are small (an application's own files; the heavy runtime is the
|
|
29
|
+
// prepended prefix, not an archive member), so each is held whole to hash it
|
|
30
|
+
// before the entry, whose comment must be fixed at creation time, is built.
|
|
31
|
+
async function* entries(members, { hashAlg, signAlg, chain }) {
|
|
32
|
+
for await (const { name, data, mode } of members) {
|
|
33
|
+
const digest = CRYPTO.createHash(hashAlg).update(data).digest('hex');
|
|
34
|
+
yield await ZLIB.ZipEntry.create(name, data, { mode: mode ?? 0o444, comment: digest });
|
|
35
|
+
}
|
|
36
|
+
yield await ZLIB.ZipEntry.create(AUTHORITY, buildManifest({ hashAlg, signAlg, chain }), { mode: 0o444 });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A Readable of the ZIP archive over `members`. Its members carry per-file
|
|
40
|
+
* digests and its AUTHORITY.PEM manifest declares the algorithms and chain, but
|
|
41
|
+
* the archive itself is left with an empty EOCD comment: the whole-file hash
|
|
42
|
+
* and its signature are a property of the finished file and are applied by
|
|
43
|
+
* `emit()`. `baseOffset` seeds the archive's internal offsets for when it is
|
|
44
|
+
* appended after a prefix.
|
|
45
|
+
*/
|
|
46
|
+
export function createArchive({ members, base, files, hashAlg = 'sha256', signAlg, chain, baseOffset = 0 }) {
|
|
47
|
+
const source = members ?? fromDirectory(base ?? '.', files ?? []);
|
|
48
|
+
return ZLIB.createZipArchive(entries(source, { hashAlg, signAlg, chain }), { baseOffset });
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A signer backed by a private key and a certificate chain already on disk —
|
|
52
|
+
* the offline-CA path, and the shape `sigstore.ts` implements too.
|
|
53
|
+
*/
|
|
54
|
+
export function keySigner({ key, chain, signAlg = 'sha256' }) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'key',
|
|
57
|
+
chain: String(chain),
|
|
58
|
+
signAlg,
|
|
59
|
+
async sign(digest) {
|
|
60
|
+
return { signature: Buffer.from(CRYPTO.sign(signAlg, digest, key)) };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Writes `prefix` (when given) then the archive to `out`, without closing
|
|
65
|
+
// `out`. With no prefix the result is a plain archive — a `.bundle` meant to be
|
|
66
|
+
// run through `--vfs-load`; with one it is a self-running container (a shebang
|
|
67
|
+
// launcher or a SEA binary) that carries the same archive in its tail. When a
|
|
68
|
+
// `signer` is given, the whole file is signed. The hash runs over the prefix
|
|
69
|
+
// and then over the archive up to (but not including) the EOCD comment; that
|
|
70
|
+
// hash is what the signer signs. The EOCD comment records both, so a verifier
|
|
71
|
+
// can validate the hash on its own (a cheap pre-mount integrity gate) and only
|
|
72
|
+
// then check the signature over that hash against the certificate:
|
|
73
|
+
//
|
|
74
|
+
// SIGNED:<hash-of-region-hex>:<signature-hex>[:<NAME>=<value>]*
|
|
75
|
+
async function emit({ members, prefix, hashAlg = 'sha256', signer, out }) {
|
|
76
|
+
const hasher = signer ? CRYPTO.createHash(hashAlg) : null;
|
|
77
|
+
// 1. Stream the prefix straight to `out`, feeding the whole-file hash.
|
|
78
|
+
if (prefix)
|
|
79
|
+
await prepend(prefix, out, hasher);
|
|
80
|
+
// 2. Build the archive (small) with an empty EOCD comment, in memory. The
|
|
81
|
+
// chain has to be embedded here, before anything is hashed — which is
|
|
82
|
+
// why a signer hands over its certificate up front and signs later.
|
|
83
|
+
const archive = await collect(createArchive({
|
|
84
|
+
members, hashAlg,
|
|
85
|
+
signAlg: signer?.signAlg,
|
|
86
|
+
chain: signer?.chain,
|
|
87
|
+
baseOffset: prefix ? FS.statSync(prefix).size : 0,
|
|
88
|
+
}));
|
|
89
|
+
if (!signer || !hasher) {
|
|
90
|
+
await write(out, archive);
|
|
91
|
+
return { hash: null, signed: false };
|
|
92
|
+
}
|
|
93
|
+
// 3. The signed region is the prefix plus the archive minus its trailing
|
|
94
|
+
// 2-byte (empty) comment-length field. Hash it, sign the hash, and
|
|
95
|
+
// re-emit the archive with the marker as the EOCD comment. Anything the
|
|
96
|
+
// signer produced *after* signing — a transparency-log entry, a
|
|
97
|
+
// timestamp — comes back as fields and rides in the same comment, since
|
|
98
|
+
// it could not have been inside the hash it postdates.
|
|
99
|
+
const region = archive.subarray(0, archive.length - 2);
|
|
100
|
+
hasher.update(region);
|
|
101
|
+
const digest = hasher.digest();
|
|
102
|
+
const { signature, fields } = await signer.sign(digest);
|
|
103
|
+
const marker = formatSignature({
|
|
104
|
+
hash: digest.toString('hex'),
|
|
105
|
+
sig: Buffer.from(signature).toString('hex'),
|
|
106
|
+
fields,
|
|
107
|
+
});
|
|
108
|
+
const comment = Buffer.from(marker, 'ascii');
|
|
109
|
+
if (comment.length > 0xffff) {
|
|
110
|
+
throw new Error(`signature marker is ${comment.length} bytes; a ZIP comment holds at most 65535`);
|
|
111
|
+
}
|
|
112
|
+
const length = Buffer.alloc(2);
|
|
113
|
+
length.writeUInt16LE(comment.length, 0);
|
|
114
|
+
await write(out, Buffer.concat([region, length, comment]));
|
|
115
|
+
return { hash: digest.toString('hex'), signed: true };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Build an archive from files on disk. `key`/`chain` are accepted as a
|
|
119
|
+
* shorthand for `signer: keySigner({ key, chain, signAlg })`, so the
|
|
120
|
+
* create-and-sign-in-one-step path stays available.
|
|
121
|
+
*/
|
|
122
|
+
export async function bundle({ base, files, prefix, hashAlg = 'sha256', signAlg = 'sha256', key, chain, signer, out }) {
|
|
123
|
+
const active = signer ?? (key && chain ? keySigner({ key, chain, signAlg }) : undefined);
|
|
124
|
+
return emit({ members: fromDirectory(base, files), prefix, hashAlg, signer: active, out });
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Re-emit an existing archive: same members, new prefix, new signature. This is
|
|
128
|
+
* what `bundle sign` runs. `source` is a path to an archive (signed or not,
|
|
129
|
+
* prefixed or not) — its members are read out, its old AUTHORITY.PEM is
|
|
130
|
+
* dropped, and everything is laid down again at the offsets the new prefix
|
|
131
|
+
* implies before the result is hashed and signed as a whole.
|
|
132
|
+
*/
|
|
133
|
+
export async function rebundle({ source, prefix, hashAlg = 'sha256', signAlg = 'sha256', key, chain, signer, out }) {
|
|
134
|
+
const active = signer ?? (key && chain ? keySigner({ key, chain, signAlg }) : undefined);
|
|
135
|
+
const zip = ZLIB.ZipFile.openSync(PATH.resolve(source));
|
|
136
|
+
try {
|
|
137
|
+
// The member list is drained into memory before writing starts: the
|
|
138
|
+
// source archive may be the file being overwritten, and in any case the
|
|
139
|
+
// entries have to outlive the ZipFile handle closed below.
|
|
140
|
+
const members = [];
|
|
141
|
+
for await (const member of fromArchive(zip))
|
|
142
|
+
members.push(member);
|
|
143
|
+
if (!members.length)
|
|
144
|
+
throw new Error(`'${source}' contains no members to sign`);
|
|
145
|
+
return await emit({ members, prefix, hashAlg, signer: active, out });
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
zip.closeSync();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The member names an archive holds, in order, excluding AUTHORITY.PEM — for
|
|
153
|
+
* reporting what is about to be re-signed without reading every member's bytes.
|
|
154
|
+
*/
|
|
155
|
+
export function members(source) {
|
|
156
|
+
const zip = ZLIB.ZipFile.openSync(PATH.resolve(source));
|
|
157
|
+
try {
|
|
158
|
+
return [...zip.entriesSync()]
|
|
159
|
+
.filter(([name, entry]) => name !== AUTHORITY && !entry.isDirectory)
|
|
160
|
+
.map(([name]) => name);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
zip.closeSync();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function prepend(file, out, sink) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
const tap = new Transform({
|
|
169
|
+
transform(chunk, _enc, cb) { if (sink)
|
|
170
|
+
sink.update(chunk); cb(null, chunk); },
|
|
171
|
+
});
|
|
172
|
+
FS.createReadStream(file).on('error', reject)
|
|
173
|
+
.pipe(tap).on('error', reject).on('end', () => resolve())
|
|
174
|
+
.pipe(out, { end: false });
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
async function collect(readable) {
|
|
178
|
+
const chunks = [];
|
|
179
|
+
for await (const chunk of readable)
|
|
180
|
+
chunks.push(chunk);
|
|
181
|
+
return Buffer.concat(chunks);
|
|
182
|
+
}
|
|
183
|
+
function write(out, buffer) {
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
out.write(buffer, (err) => (err ? reject(err) : resolve()));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=archive.js.map
|