@orbit-tools/cli 0.6.0 → 0.7.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/README.md +9 -5
- package/package.json +2 -1
- package/release-signing.pub +11 -0
- package/scripts/install-binary.js +297 -26
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
npm proxy for the [Orbit](https://github.com/danieljhkim/orbit) CLI.
|
|
4
4
|
|
|
5
5
|
On install, downloads the matching prebuilt `orbit` binary from
|
|
6
|
-
[GitHub Releases](https://github.com/danieljhkim/orbit/releases),
|
|
7
|
-
|
|
8
|
-
`orbit` command.
|
|
6
|
+
[GitHub Releases](https://github.com/danieljhkim/orbit/releases), authenticates
|
|
7
|
+
the signed `orbit-checksums.txt` with the package-pinned release trust set,
|
|
8
|
+
verifies the archive SHA-256, and exposes it as the `orbit` command.
|
|
9
9
|
|
|
10
10
|
## Usage
|
|
11
11
|
|
|
@@ -31,8 +31,12 @@ Windows is not currently published. Use WSL or build from source.
|
|
|
31
31
|
|
|
32
32
|
| Variable | Effect |
|
|
33
33
|
|---|---|
|
|
34
|
-
| `ORBIT_BINARY` | Path to a local `orbit` binary; bypasses download. |
|
|
35
|
-
| `ORBIT_BINARY_VERSION` | Override the release tag to install (e.g. `v0.3.1`). |
|
|
34
|
+
| `ORBIT_BINARY` | Path to a local `orbit` binary; bypasses download and trusts that path as the binary source. |
|
|
35
|
+
| `ORBIT_BINARY_VERSION` | Override the release tag to install (e.g. `v0.3.1`); this changes the release-selection trust boundary but still requires a valid Orbit checksum signature. |
|
|
36
|
+
| `ORBIT_RELEASE_PUBLIC_KEY_FILE` | **Deprecated** in favor of `ORBIT_RELEASE_TRUSTED_KEYS_FILE`. Single-key override for the trusted checksum-signing public key; requires `ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE=1`, logs a deprecation notice when active. |
|
|
37
|
+
| `ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` | Required acknowledgement that `ORBIT_RELEASE_PUBLIC_KEY_FILE` replaces the release authenticity trust root. |
|
|
38
|
+
| `ORBIT_RELEASE_TRUSTED_KEYS_FILE` | Preferred test/operations override for the full trusted signing-key set, including key IDs, `not_after`, and `revoked_at`; requires `ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` and logs when active. |
|
|
39
|
+
| `ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` | Required acknowledgement that `ORBIT_RELEASE_TRUSTED_KEYS_FILE` replaces the release authenticity trust root. |
|
|
36
40
|
| `ORBIT_SKIP_DOWNLOAD=1` | Skip postinstall download (lazy install on first run still works). |
|
|
37
41
|
|
|
38
42
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbit-tools/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "npm proxy for the Orbit CLI. Downloads the matching native binary from GitHub Releases on install and forwards all invocations.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"orbit": "bin/orbit.js"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"bin/",
|
|
14
14
|
"scripts/",
|
|
15
|
+
"release-signing.pub",
|
|
15
16
|
"README.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-----BEGIN PUBLIC KEY-----
|
|
2
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAuZ8vNa+DusYhrFBXNhBh
|
|
3
|
+
RSqn81AYe7tYEtCKImWGuy/6ziMHqDzDKHSku0sBMwcdLXBzI0RjNBacLCbbYr4H
|
|
4
|
+
icmYrsKqqfLGf+CWfrqDqY9d3hwUPtVMRp/ynVNW6nwKAmNl5dTgUc6ZBAZTtQtt
|
|
5
|
+
qwMD1JIOsrJ3vVDL9o3alcXcg/RyL0pGUo+vep2QZOjXnCGoJN3NeytQHag3zJyd
|
|
6
|
+
Wq4psc7j2H1Nb5EoyY/I/7vpdwME3Mrv2ffwtDmr0/+73q1yWUDf4btY9Ba7sOhE
|
|
7
|
+
Ir2UHm3bEboo1ErAYjjiDDuF/NjzZcZpJtuNbdj0vI7pHDyDZ7sKiEX7RkUO+e2c
|
|
8
|
+
IouiSfRJRrwnjpuergrq3ehNjkxcn5dFST1l23FOXGsy4F7ilrF6P9cgaAsE8dc7
|
|
9
|
+
CS9YgUE1ErfGJLZtfDDGKs6+E+7JiC1C3z7xwmfzOgv9gEvSlfrx2BbGl8esypKm
|
|
10
|
+
pYZDkW2dLqPeFj/WwGhZoYFHv0GOMIWdi6FNriQdkn4RAgMBAAE=
|
|
11
|
+
-----END PUBLIC KEY-----
|
|
@@ -10,12 +10,60 @@ const os = require('node:os');
|
|
|
10
10
|
|
|
11
11
|
const PKG = require('../package.json');
|
|
12
12
|
const BINARY_REPO = PKG.config.orbit.binaryRepo;
|
|
13
|
+
const CHECKSUM_FILE = 'orbit-checksums.txt';
|
|
14
|
+
const CHECKSUM_SIGNATURE_FILE = 'orbit-checksums.txt.sig';
|
|
13
15
|
// Convention: npm package version is kept in lockstep with the orbit release tag.
|
|
14
16
|
// `0.3.1` → fetches `v0.3.1` from GitHub Releases. Override with $ORBIT_BINARY_VERSION.
|
|
15
17
|
const BINARY_VERSION = process.env.ORBIT_BINARY_VERSION || `v${PKG.version}`;
|
|
16
18
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
17
19
|
const BIN_DIR = path.join(PKG_ROOT, 'binaries');
|
|
18
20
|
const BIN_PATH = path.join(BIN_DIR, process.platform === 'win32' ? 'orbit.exe' : 'orbit');
|
|
21
|
+
const PUBLIC_KEY_OVERRIDE = process.env.ORBIT_RELEASE_PUBLIC_KEY_FILE;
|
|
22
|
+
const PUBLIC_KEY_OVERRIDE_ACK_ENV = 'ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE';
|
|
23
|
+
const TRUSTED_KEYS_OVERRIDE = process.env.ORBIT_RELEASE_TRUSTED_KEYS_FILE;
|
|
24
|
+
const TRUSTED_KEYS_OVERRIDE_ACK_ENV = 'ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE';
|
|
25
|
+
const TRUSTED_PUBLIC_KEY_PATH = PUBLIC_KEY_OVERRIDE
|
|
26
|
+
? path.resolve(PUBLIC_KEY_OVERRIDE)
|
|
27
|
+
: path.join(PKG_ROOT, 'release-signing.pub');
|
|
28
|
+
const TRUSTED_KEYS_OVERRIDE_PATH = TRUSTED_KEYS_OVERRIDE ? path.resolve(TRUSTED_KEYS_OVERRIDE) : null;
|
|
29
|
+
const TRUSTED_RELEASE_KEYS = Object.freeze([
|
|
30
|
+
Object.freeze({
|
|
31
|
+
id: 'orbit-release-key-1',
|
|
32
|
+
notAfter: '2027-12-31',
|
|
33
|
+
revokedAt: null,
|
|
34
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
35
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAuZ8vNa+DusYhrFBXNhBh
|
|
36
|
+
RSqn81AYe7tYEtCKImWGuy/6ziMHqDzDKHSku0sBMwcdLXBzI0RjNBacLCbbYr4H
|
|
37
|
+
icmYrsKqqfLGf+CWfrqDqY9d3hwUPtVMRp/ynVNW6nwKAmNl5dTgUc6ZBAZTtQtt
|
|
38
|
+
qwMD1JIOsrJ3vVDL9o3alcXcg/RyL0pGUo+vep2QZOjXnCGoJN3NeytQHag3zJyd
|
|
39
|
+
Wq4psc7j2H1Nb5EoyY/I/7vpdwME3Mrv2ffwtDmr0/+73q1yWUDf4btY9Ba7sOhE
|
|
40
|
+
Ir2UHm3bEboo1ErAYjjiDDuF/NjzZcZpJtuNbdj0vI7pHDyDZ7sKiEX7RkUO+e2c
|
|
41
|
+
IouiSfRJRrwnjpuergrq3ehNjkxcn5dFST1l23FOXGsy4F7ilrF6P9cgaAsE8dc7
|
|
42
|
+
CS9YgUE1ErfGJLZtfDDGKs6+E+7JiC1C3z7xwmfzOgv9gEvSlfrx2BbGl8esypKm
|
|
43
|
+
pYZDkW2dLqPeFj/WwGhZoYFHv0GOMIWdi6FNriQdkn4RAgMBAAE=
|
|
44
|
+
-----END PUBLIC KEY-----
|
|
45
|
+
`,
|
|
46
|
+
}),
|
|
47
|
+
Object.freeze({
|
|
48
|
+
id: 'orbit-release-key-2',
|
|
49
|
+
notAfter: '2028-12-31',
|
|
50
|
+
revokedAt: null,
|
|
51
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
52
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA22OLkoJXb7PX/QwG7FzA
|
|
53
|
+
eiDT0BctOq77WyXysUBlN1718pbjo30wLIW9d2h+cxPtld2PM35NQ4NMZJwl7lb2
|
|
54
|
+
tZSWVUTRI9Se5eBhUEL6Gi4Rin4/In3NIx0YEVdA6SUA+x3OinPpe1BIN1pKGbpD
|
|
55
|
+
P6GohwmrdbCUuZMv29UYPkREif6gnlehxj9ypZfZMVU7+VXOceeA5OT5iXbO4u29
|
|
56
|
+
85bSys4JUN+SKtAao9BAjHCvMUJ4kL4mnGeteXRssmd5ehkEezUhWUNtP/uvKv+I
|
|
57
|
+
5pjSto5vq6vqZMEpkOPDANPSUwz3F0CgyNmwHU8LKHtME+ZQqOP11xOC5Rgtw3zZ
|
|
58
|
+
VQSAd3BLsSflu7ENV9lsbwPjvhSRDPlZsGUB4NGjUmFkUJPz7SGqT9LNXQqaRT2S
|
|
59
|
+
8mHajs8Z/pkIMnOSOE339DHmT3xPz5eKRwZLBWttguJxHWFM50PT7g+K97Y6n9Zr
|
|
60
|
+
zLEax5f3s3F9vPiWfA36hKJS5GzF564hCRViOQPqWnNlAgMBAAE=
|
|
61
|
+
-----END PUBLIC KEY-----
|
|
62
|
+
`,
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
let publicKeyOverrideLogged = false;
|
|
66
|
+
let trustedKeysOverrideLogged = false;
|
|
19
67
|
|
|
20
68
|
function log(msg) {
|
|
21
69
|
process.stderr.write(`@orbit-tools/cli: ${msg}\n`);
|
|
@@ -78,13 +126,215 @@ function parseChecksums(text) {
|
|
|
78
126
|
return out;
|
|
79
127
|
}
|
|
80
128
|
|
|
81
|
-
function
|
|
82
|
-
|
|
129
|
+
function acknowledgeTrustedPublicKeyOverride() {
|
|
130
|
+
if (PUBLIC_KEY_OVERRIDE && TRUSTED_KEYS_OVERRIDE) {
|
|
131
|
+
throw new Error('ORBIT_RELEASE_PUBLIC_KEY_FILE and ORBIT_RELEASE_TRUSTED_KEYS_FILE cannot both be set');
|
|
132
|
+
}
|
|
133
|
+
if (!PUBLIC_KEY_OVERRIDE) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (process.env[PUBLIC_KEY_OVERRIDE_ACK_ENV] !== '1') {
|
|
137
|
+
throw new Error(`ORBIT_RELEASE_PUBLIC_KEY_FILE requires ${PUBLIC_KEY_OVERRIDE_ACK_ENV}=1`);
|
|
138
|
+
}
|
|
139
|
+
if (!publicKeyOverrideLogged) {
|
|
140
|
+
log(`ORBIT_RELEASE_PUBLIC_KEY_FILE=${TRUSTED_PUBLIC_KEY_PATH} set; trusting replacement release signing key`);
|
|
141
|
+
log('ORBIT_RELEASE_PUBLIC_KEY_FILE is deprecated; prefer ORBIT_RELEASE_TRUSTED_KEYS_FILE for the full trust set (key IDs, notAfter, revokedAt)');
|
|
142
|
+
publicKeyOverrideLogged = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function acknowledgeTrustedKeysOverride() {
|
|
147
|
+
if (PUBLIC_KEY_OVERRIDE && TRUSTED_KEYS_OVERRIDE) {
|
|
148
|
+
throw new Error('ORBIT_RELEASE_PUBLIC_KEY_FILE and ORBIT_RELEASE_TRUSTED_KEYS_FILE cannot both be set');
|
|
149
|
+
}
|
|
150
|
+
if (!TRUSTED_KEYS_OVERRIDE) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (process.env[TRUSTED_KEYS_OVERRIDE_ACK_ENV] !== '1') {
|
|
154
|
+
throw new Error(`ORBIT_RELEASE_TRUSTED_KEYS_FILE requires ${TRUSTED_KEYS_OVERRIDE_ACK_ENV}=1`);
|
|
155
|
+
}
|
|
156
|
+
if (!trustedKeysOverrideLogged) {
|
|
157
|
+
log(`ORBIT_RELEASE_TRUSTED_KEYS_FILE=${TRUSTED_KEYS_OVERRIDE_PATH} set; trusting replacement release signing key set`);
|
|
158
|
+
trustedKeysOverrideLogged = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readTrustedKeysManifest(manifestPath) {
|
|
163
|
+
const manifestDir = path.dirname(manifestPath);
|
|
164
|
+
return fs
|
|
165
|
+
.readFileSync(manifestPath, 'utf8')
|
|
166
|
+
.split(/\r?\n/)
|
|
167
|
+
.map((line) => line.trim())
|
|
168
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'))
|
|
169
|
+
.map((line) => {
|
|
170
|
+
const [id, notAfter, revokedAt, publicKeyPath, ...extra] = line.split('|');
|
|
171
|
+
if (!id || !publicKeyPath || extra.length > 0) {
|
|
172
|
+
throw new Error(`invalid trusted release signing key record: ${line}`);
|
|
173
|
+
}
|
|
174
|
+
const resolvedPublicKeyPath = path.isAbsolute(publicKeyPath)
|
|
175
|
+
? publicKeyPath
|
|
176
|
+
: path.resolve(manifestDir, publicKeyPath);
|
|
177
|
+
return {
|
|
178
|
+
id,
|
|
179
|
+
notAfter: notAfter || null,
|
|
180
|
+
revokedAt: revokedAt || null,
|
|
181
|
+
publicKeyPem: fs.readFileSync(resolvedPublicKeyPath, 'utf8'),
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function normalizeTrustedReleaseKeys(trustedKeys) {
|
|
187
|
+
if (typeof trustedKeys === 'string') {
|
|
188
|
+
return [
|
|
189
|
+
{
|
|
190
|
+
id: 'override',
|
|
191
|
+
notAfter: null,
|
|
192
|
+
revokedAt: null,
|
|
193
|
+
publicKeyPem: trustedKeys,
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!Array.isArray(trustedKeys) || trustedKeys.length === 0) {
|
|
199
|
+
throw new Error('at least one trusted release signing key is required');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return trustedKeys.map((key) => {
|
|
203
|
+
if (!key || !key.id || !key.publicKeyPem) {
|
|
204
|
+
throw new Error('trusted release signing keys require id and publicKeyPem');
|
|
205
|
+
}
|
|
206
|
+
const notAfter = key.notAfter || null;
|
|
207
|
+
const revokedAt = key.revokedAt || null;
|
|
208
|
+
// Mirror the shell-side awk regex (release_date_number) so a malformed
|
|
209
|
+
// override like notAfter: "next month" fails closed instead of silently
|
|
210
|
+
// becoming "never expires" under lexicographic comparison.
|
|
211
|
+
if (notAfter !== null && !/^\d{4}-\d{2}-\d{2}$/.test(notAfter)) {
|
|
212
|
+
throw new Error(`trusted release signing key ${key.id} has invalid notAfter: ${notAfter} (expected YYYY-MM-DD)`);
|
|
213
|
+
}
|
|
214
|
+
if (revokedAt !== null && !/^\d{4}-\d{2}-\d{2}$/.test(revokedAt)) {
|
|
215
|
+
throw new Error(`trusted release signing key ${key.id} has invalid revokedAt: ${revokedAt} (expected YYYY-MM-DD)`);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
id: key.id,
|
|
219
|
+
notAfter,
|
|
220
|
+
revokedAt,
|
|
221
|
+
publicKeyPem: key.publicKeyPem,
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readTrustedReleaseKeys() {
|
|
227
|
+
acknowledgeTrustedPublicKeyOverride();
|
|
228
|
+
acknowledgeTrustedKeysOverride();
|
|
229
|
+
|
|
230
|
+
if (PUBLIC_KEY_OVERRIDE) {
|
|
231
|
+
return normalizeTrustedReleaseKeys(fs.readFileSync(TRUSTED_PUBLIC_KEY_PATH, 'utf8'));
|
|
232
|
+
}
|
|
233
|
+
if (TRUSTED_KEYS_OVERRIDE_PATH) {
|
|
234
|
+
return normalizeTrustedReleaseKeys(readTrustedKeysManifest(TRUSTED_KEYS_OVERRIDE_PATH));
|
|
235
|
+
}
|
|
236
|
+
return TRUSTED_RELEASE_KEYS;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function releaseDateString(date = new Date()) {
|
|
240
|
+
return date.toISOString().slice(0, 10);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function assertTrustedKeyUsable(key, verificationDate = new Date()) {
|
|
244
|
+
if (key.revokedAt) {
|
|
245
|
+
throw new Error(`release checksum signature was made by revoked release signing key ${key.id} (revoked ${key.revokedAt})`);
|
|
246
|
+
}
|
|
247
|
+
if (key.notAfter && releaseDateString(verificationDate) > key.notAfter) {
|
|
248
|
+
throw new Error(`release checksum signature was made by expired release signing key ${key.id} (not_after ${key.notAfter})`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function verifyWithPublicKey(checksumText, signatureBuf, publicKeyPem) {
|
|
253
|
+
const verifier = crypto.createVerify('RSA-SHA256');
|
|
254
|
+
verifier.update(checksumText, 'utf8');
|
|
255
|
+
verifier.end();
|
|
256
|
+
return verifier.verify(publicKeyPem, signatureBuf);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function verifyChecksumSignature(
|
|
260
|
+
checksumText,
|
|
261
|
+
signatureBuf,
|
|
262
|
+
trustedKeys = readTrustedReleaseKeys(),
|
|
263
|
+
verificationDate = new Date()
|
|
264
|
+
) {
|
|
265
|
+
for (const key of normalizeTrustedReleaseKeys(trustedKeys)) {
|
|
266
|
+
if (verifyWithPublicKey(checksumText, signatureBuf, key.publicKeyPem)) {
|
|
267
|
+
assertTrustedKeyUsable(key, verificationDate);
|
|
268
|
+
log(`authenticated ${CHECKSUM_FILE} with release signing key ${key.id}`);
|
|
269
|
+
return key.id;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`release checksum signature verification failed for ${CHECKSUM_FILE}: no trusted release signing key matched`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function verifyArchiveChecksum(asset, archiveBuf, checksumText) {
|
|
276
|
+
const checksums = parseChecksums(checksumText);
|
|
277
|
+
const expected = checksums[asset];
|
|
278
|
+
if (!expected) {
|
|
279
|
+
throw new Error(`checksum entry for ${asset} was not found in ${CHECKSUM_FILE}`);
|
|
280
|
+
}
|
|
281
|
+
const actual = sha256(archiveBuf);
|
|
282
|
+
if (actual !== expected) {
|
|
283
|
+
throw new Error(`checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function runTar(args, options = {}) {
|
|
288
|
+
const result = spawnSync('tar', args, { encoding: 'utf8', ...options });
|
|
289
|
+
if (result.error) {
|
|
290
|
+
throw result.error;
|
|
291
|
+
}
|
|
83
292
|
if (result.status !== 0) {
|
|
84
|
-
|
|
293
|
+
const stderr = result.stderr ? `: ${result.stderr.trim()}` : '';
|
|
294
|
+
throw new Error(`tar ${args.join(' ')} failed (status ${result.status})${stderr}`);
|
|
295
|
+
}
|
|
296
|
+
return result.stdout || '';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function validateArchiveMemberName(member) {
|
|
300
|
+
if (member === 'orbit') {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!member || member.startsWith('/') || member.includes('..') || path.isAbsolute(member)) {
|
|
304
|
+
throw new Error(`unsafe release archive member: ${member || '<empty>'}`);
|
|
305
|
+
}
|
|
306
|
+
throw new Error(`unexpected release archive member: ${member}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function validateArchiveMembers(archivePath) {
|
|
310
|
+
const members = runTar(['-tzf', archivePath])
|
|
311
|
+
.split(/\r?\n/)
|
|
312
|
+
.filter((line) => line.length > 0);
|
|
313
|
+
if (members.length !== 1) {
|
|
314
|
+
throw new Error("release archive must contain only the 'orbit' binary");
|
|
315
|
+
}
|
|
316
|
+
validateArchiveMemberName(members[0]);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function validateExtractedBinary(extractedBinary) {
|
|
320
|
+
const stat = fs.lstatSync(extractedBinary);
|
|
321
|
+
if (stat.isSymbolicLink()) {
|
|
322
|
+
throw new Error('release archive member is a symlink: orbit');
|
|
323
|
+
}
|
|
324
|
+
if (!stat.isFile()) {
|
|
325
|
+
throw new Error('release archive member must be a regular file: orbit');
|
|
326
|
+
}
|
|
327
|
+
if (stat.nlink !== 1) {
|
|
328
|
+
throw new Error('release archive member must not be a hard link: orbit');
|
|
85
329
|
}
|
|
86
330
|
}
|
|
87
331
|
|
|
332
|
+
function extractTarGz(archivePath, destDir) {
|
|
333
|
+
validateArchiveMembers(archivePath);
|
|
334
|
+
runTar(['-xzf', archivePath, '-C', destDir, 'orbit'], { stdio: 'inherit' });
|
|
335
|
+
validateExtractedBinary(path.join(destDir, 'orbit'));
|
|
336
|
+
}
|
|
337
|
+
|
|
88
338
|
async function main() {
|
|
89
339
|
if (process.env.ORBIT_SKIP_DOWNLOAD === '1') {
|
|
90
340
|
log('ORBIT_SKIP_DOWNLOAD=1 set; skipping binary download.');
|
|
@@ -99,12 +349,23 @@ async function main() {
|
|
|
99
349
|
const asset = `orbit-${target}.tar.gz`;
|
|
100
350
|
const baseUrl = `https://github.com/${BINARY_REPO}/releases/download/${BINARY_VERSION}`;
|
|
101
351
|
const archiveUrl = `${baseUrl}/${asset}`;
|
|
102
|
-
const checksumUrl = `${baseUrl}
|
|
352
|
+
const checksumUrl = `${baseUrl}/${CHECKSUM_FILE}`;
|
|
353
|
+
const checksumSignatureUrl = `${baseUrl}/${CHECKSUM_SIGNATURE_FILE}`;
|
|
103
354
|
|
|
104
355
|
log(`installing orbit ${BINARY_VERSION} for ${target}...`);
|
|
105
356
|
|
|
106
357
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
107
358
|
|
|
359
|
+
let checksumText;
|
|
360
|
+
let signatureBuf;
|
|
361
|
+
try {
|
|
362
|
+
checksumText = (await fetchBuffer(checksumUrl)).toString('utf8');
|
|
363
|
+
signatureBuf = await fetchBuffer(checksumSignatureUrl);
|
|
364
|
+
verifyChecksumSignature(checksumText, signatureBuf);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
fail(`could not authenticate ${CHECKSUM_FILE}: ${err.message}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
108
369
|
let archiveBuf;
|
|
109
370
|
try {
|
|
110
371
|
archiveBuf = await fetchBuffer(archiveUrl);
|
|
@@ -113,35 +374,45 @@ async function main() {
|
|
|
113
374
|
}
|
|
114
375
|
|
|
115
376
|
try {
|
|
116
|
-
|
|
117
|
-
const checksums = parseChecksums(checksumText);
|
|
118
|
-
const expected = checksums[asset];
|
|
119
|
-
if (!expected) {
|
|
120
|
-
fail(`checksum entry for ${asset} was not found in orbit-checksums.txt`);
|
|
121
|
-
} else {
|
|
122
|
-
const actual = sha256(archiveBuf);
|
|
123
|
-
if (actual !== expected) {
|
|
124
|
-
fail(`checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
377
|
+
verifyArchiveChecksum(asset, archiveBuf, checksumText);
|
|
127
378
|
} catch (err) {
|
|
128
|
-
fail(
|
|
379
|
+
fail(err.message);
|
|
129
380
|
}
|
|
130
381
|
|
|
131
382
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orbit-cli-'));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
383
|
+
try {
|
|
384
|
+
const archivePath = path.join(tmpDir, asset);
|
|
385
|
+
fs.writeFileSync(archivePath, archiveBuf);
|
|
386
|
+
extractTarGz(archivePath, tmpDir);
|
|
135
387
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
388
|
+
const extractedBinary = path.join(tmpDir, 'orbit');
|
|
389
|
+
if (!fs.existsSync(extractedBinary)) {
|
|
390
|
+
fail(`extracted archive did not contain 'orbit' binary at ${extractedBinary}`);
|
|
391
|
+
}
|
|
392
|
+
fs.copyFileSync(extractedBinary, BIN_PATH);
|
|
393
|
+
fs.chmodSync(BIN_PATH, 0o755);
|
|
394
|
+
} finally {
|
|
395
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
139
396
|
}
|
|
140
|
-
fs.copyFileSync(extractedBinary, BIN_PATH);
|
|
141
|
-
fs.chmodSync(BIN_PATH, 0o755);
|
|
142
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
143
397
|
|
|
144
398
|
log(`installed orbit binary at ${BIN_PATH}`);
|
|
145
399
|
}
|
|
146
400
|
|
|
147
|
-
|
|
401
|
+
if (require.main === module) {
|
|
402
|
+
main().catch((err) => fail(err && err.message ? err.message : String(err)));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = {
|
|
406
|
+
TRUSTED_RELEASE_KEYS,
|
|
407
|
+
parseChecksums,
|
|
408
|
+
sha256,
|
|
409
|
+
acknowledgeTrustedPublicKeyOverride,
|
|
410
|
+
acknowledgeTrustedKeysOverride,
|
|
411
|
+
extractTarGz,
|
|
412
|
+
normalizeTrustedReleaseKeys,
|
|
413
|
+
readTrustedReleaseKeys,
|
|
414
|
+
validateArchiveMembers,
|
|
415
|
+
validateExtractedBinary,
|
|
416
|
+
verifyArchiveChecksum,
|
|
417
|
+
verifyChecksumSignature,
|
|
418
|
+
};
|