@orbit-tools/cli 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/package.json +2 -1
- package/release-signing.pub +11 -0
- package/scripts/install-binary.js +307 -29
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,11 @@ 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
|
-
| `
|
|
34
|
+
| `ORBIT_BINARY` | Path to a local `orbit` binary; bypasses download and trusts that path as the binary source. |
|
|
35
|
+
| `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. |
|
|
36
|
+
| `ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` | Required acknowledgement that `ORBIT_RELEASE_PUBLIC_KEY_FILE` replaces the release authenticity trust root. |
|
|
37
|
+
| `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. |
|
|
38
|
+
| `ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE=1` | Required acknowledgement that `ORBIT_RELEASE_TRUSTED_KEYS_FILE` replaces the release authenticity trust root. |
|
|
36
39
|
| `ORBIT_SKIP_DOWNLOAD=1` | Skip postinstall download (lazy install on first run still works). |
|
|
37
40
|
|
|
38
41
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbit-tools/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAoQGLKOvvsvXriGIQ0oxA
|
|
3
|
+
PcDyVHLM1iqXBCYXg+blQU41haEkG1eYabvDfeGcyGaC4awW7Q2uCZK05+/Hdjpe
|
|
4
|
+
cRUVxP+QWKCAHyretQwOsoXzutZjJgId/ZRiUJPS/FeJOSv0xrayaol0tmfeJ4mH
|
|
5
|
+
gFseCLq+mIIWIPRvXmYiKaUB//bjF79w/m4VXlyBhfi6n+f6x2UPG+gjjsjwG6mn
|
|
6
|
+
Orec31AAFCIIX69YAd21D3MBc4S89/LoYZCq3neDscZ09Y+e6Jg2HpoBstvqSnq/
|
|
7
|
+
3s34unLuIRlyB8jyK8CrdzT1E6YVB7+riAjycE9XMlLOQ2xA4tl6CKIx5YTKHyeW
|
|
8
|
+
npMLlbzNaVfFT7p3IPTxsoEI0SB3ZtO7/XhzuOvOpklYcqjW2DGw/yzr2epAqHE/
|
|
9
|
+
y4rLO3hkxWhxfgF5KPSR2iftc3LMONRGWELK6jpD5KB7No5vwIvjpVPUc5xA45Xw
|
|
10
|
+
tT/bo0mm4TvrumxYr1xyEHrdum+ej/WYz/0BZQlwDOtXAgMBAAE=
|
|
11
|
+
-----END PUBLIC KEY-----
|
|
@@ -10,12 +10,67 @@ const os = require('node:os');
|
|
|
10
10
|
|
|
11
11
|
const PKG = require('../package.json');
|
|
12
12
|
const BINARY_REPO = PKG.config.orbit.binaryRepo;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const CHECKSUM_FILE = 'orbit-checksums.txt';
|
|
14
|
+
const CHECKSUM_SIGNATURE_FILE = 'orbit-checksums.txt.sig';
|
|
15
|
+
// npm package version is kept in lockstep with the orbit release tag. `0.3.1`
|
|
16
|
+
// → fetches `v0.3.1` from GitHub Releases. The mapping is intentionally strict:
|
|
17
|
+
// older orbit binaries can no longer be installed through this package, so the
|
|
18
|
+
// trust set only needs to cover the current release.
|
|
19
|
+
const BINARY_VERSION = `v${PKG.version}`;
|
|
16
20
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
17
21
|
const BIN_DIR = path.join(PKG_ROOT, 'binaries');
|
|
18
22
|
const BIN_PATH = path.join(BIN_DIR, process.platform === 'win32' ? 'orbit.exe' : 'orbit');
|
|
23
|
+
const PUBLIC_KEY_OVERRIDE = process.env.ORBIT_RELEASE_PUBLIC_KEY_FILE;
|
|
24
|
+
const PUBLIC_KEY_OVERRIDE_ACK_ENV = 'ORBIT_RELEASE_PUBLIC_KEY_FILE_ACKNOWLEDGE_TRUST_CHANGE';
|
|
25
|
+
const TRUSTED_KEYS_OVERRIDE = process.env.ORBIT_RELEASE_TRUSTED_KEYS_FILE;
|
|
26
|
+
const TRUSTED_KEYS_OVERRIDE_ACK_ENV = 'ORBIT_RELEASE_TRUSTED_KEYS_FILE_ACKNOWLEDGE_TRUST_CHANGE';
|
|
27
|
+
const TRUSTED_PUBLIC_KEY_PATH = PUBLIC_KEY_OVERRIDE
|
|
28
|
+
? path.resolve(PUBLIC_KEY_OVERRIDE)
|
|
29
|
+
: path.join(PKG_ROOT, 'release-signing.pub');
|
|
30
|
+
const TRUSTED_KEYS_OVERRIDE_PATH = TRUSTED_KEYS_OVERRIDE ? path.resolve(TRUSTED_KEYS_OVERRIDE) : null;
|
|
31
|
+
// orbit-release-key-4 is a PLACEHOLDER pre-staged for the next rotation. The
|
|
32
|
+
// PEM below was generated locally and the matching private key is NOT held by
|
|
33
|
+
// release infrastructure — no production signature will ever verify against
|
|
34
|
+
// it. Replace with a real keypair (generated on the signing host) before
|
|
35
|
+
// rotating off key-3.
|
|
36
|
+
const TRUSTED_RELEASE_KEYS = Object.freeze([
|
|
37
|
+
Object.freeze({
|
|
38
|
+
id: 'orbit-release-key-3',
|
|
39
|
+
notAfter: '2029-12-31',
|
|
40
|
+
revokedAt: null,
|
|
41
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
42
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAoQGLKOvvsvXriGIQ0oxA
|
|
43
|
+
PcDyVHLM1iqXBCYXg+blQU41haEkG1eYabvDfeGcyGaC4awW7Q2uCZK05+/Hdjpe
|
|
44
|
+
cRUVxP+QWKCAHyretQwOsoXzutZjJgId/ZRiUJPS/FeJOSv0xrayaol0tmfeJ4mH
|
|
45
|
+
gFseCLq+mIIWIPRvXmYiKaUB//bjF79w/m4VXlyBhfi6n+f6x2UPG+gjjsjwG6mn
|
|
46
|
+
Orec31AAFCIIX69YAd21D3MBc4S89/LoYZCq3neDscZ09Y+e6Jg2HpoBstvqSnq/
|
|
47
|
+
3s34unLuIRlyB8jyK8CrdzT1E6YVB7+riAjycE9XMlLOQ2xA4tl6CKIx5YTKHyeW
|
|
48
|
+
npMLlbzNaVfFT7p3IPTxsoEI0SB3ZtO7/XhzuOvOpklYcqjW2DGw/yzr2epAqHE/
|
|
49
|
+
y4rLO3hkxWhxfgF5KPSR2iftc3LMONRGWELK6jpD5KB7No5vwIvjpVPUc5xA45Xw
|
|
50
|
+
tT/bo0mm4TvrumxYr1xyEHrdum+ej/WYz/0BZQlwDOtXAgMBAAE=
|
|
51
|
+
-----END PUBLIC KEY-----
|
|
52
|
+
`,
|
|
53
|
+
}),
|
|
54
|
+
Object.freeze({
|
|
55
|
+
id: 'orbit-release-key-4',
|
|
56
|
+
notAfter: '2030-12-31',
|
|
57
|
+
revokedAt: null,
|
|
58
|
+
publicKeyPem: `-----BEGIN PUBLIC KEY-----
|
|
59
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAiEVVbwQYDnbPg86xYrI8
|
|
60
|
+
Ddm6qpkEJ6GSJOW9NfR/eLpqwwaeWb3EPR9H/U39Rrt8ABPAGObLG9vuvSzg8YqU
|
|
61
|
+
Rz6NjtrSKQ4k9xO/up+qQ/zRsHkOyISEx+6MnIrw5hY/IfkrZ+3+jm8IfXJ+VAjS
|
|
62
|
+
VepR+o58u73ycrnLG1eXWIHtjED3SQSPffJjxSvVDEb3ogiJAsWClCMWNLnEjsQc
|
|
63
|
+
IHYrNdS5N0m5tfcIb9LiV2cDVXgdwdRUU41Ks9sWvBQIHrNup721UWyMdJoK1hTI
|
|
64
|
+
rc4PST3WTHQwFcQvVaAqod9MDPkQYlgD7IjiPkSGLHyIs52kNgFXY55E5DlU4O4e
|
|
65
|
+
9QDbyQTGzZI0XlnoqAuCIXbcNXjMZuEn9UjVN35NeObsj6F/yL07YUhvORnxozjL
|
|
66
|
+
41ouRFtFTWFHNenthtZnH9SUV4+O2cKDmtJpPJd68ZJ/NBqJHM4a6cteT72HJzLb
|
|
67
|
+
t6yto3B43nTeXtp9ozRjetznPnPD7gmI6Zq1P2ce8v49AgMBAAE=
|
|
68
|
+
-----END PUBLIC KEY-----
|
|
69
|
+
`,
|
|
70
|
+
}),
|
|
71
|
+
]);
|
|
72
|
+
let publicKeyOverrideLogged = false;
|
|
73
|
+
let trustedKeysOverrideLogged = false;
|
|
19
74
|
|
|
20
75
|
function log(msg) {
|
|
21
76
|
process.stderr.write(`@orbit-tools/cli: ${msg}\n`);
|
|
@@ -78,13 +133,215 @@ function parseChecksums(text) {
|
|
|
78
133
|
return out;
|
|
79
134
|
}
|
|
80
135
|
|
|
81
|
-
function
|
|
82
|
-
|
|
136
|
+
function acknowledgeTrustedPublicKeyOverride() {
|
|
137
|
+
if (PUBLIC_KEY_OVERRIDE && TRUSTED_KEYS_OVERRIDE) {
|
|
138
|
+
throw new Error('ORBIT_RELEASE_PUBLIC_KEY_FILE and ORBIT_RELEASE_TRUSTED_KEYS_FILE cannot both be set');
|
|
139
|
+
}
|
|
140
|
+
if (!PUBLIC_KEY_OVERRIDE) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (process.env[PUBLIC_KEY_OVERRIDE_ACK_ENV] !== '1') {
|
|
144
|
+
throw new Error(`ORBIT_RELEASE_PUBLIC_KEY_FILE requires ${PUBLIC_KEY_OVERRIDE_ACK_ENV}=1`);
|
|
145
|
+
}
|
|
146
|
+
if (!publicKeyOverrideLogged) {
|
|
147
|
+
log(`ORBIT_RELEASE_PUBLIC_KEY_FILE=${TRUSTED_PUBLIC_KEY_PATH} set; trusting replacement release signing key`);
|
|
148
|
+
log('ORBIT_RELEASE_PUBLIC_KEY_FILE is deprecated; prefer ORBIT_RELEASE_TRUSTED_KEYS_FILE for the full trust set (key IDs, notAfter, revokedAt)');
|
|
149
|
+
publicKeyOverrideLogged = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function acknowledgeTrustedKeysOverride() {
|
|
154
|
+
if (PUBLIC_KEY_OVERRIDE && TRUSTED_KEYS_OVERRIDE) {
|
|
155
|
+
throw new Error('ORBIT_RELEASE_PUBLIC_KEY_FILE and ORBIT_RELEASE_TRUSTED_KEYS_FILE cannot both be set');
|
|
156
|
+
}
|
|
157
|
+
if (!TRUSTED_KEYS_OVERRIDE) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (process.env[TRUSTED_KEYS_OVERRIDE_ACK_ENV] !== '1') {
|
|
161
|
+
throw new Error(`ORBIT_RELEASE_TRUSTED_KEYS_FILE requires ${TRUSTED_KEYS_OVERRIDE_ACK_ENV}=1`);
|
|
162
|
+
}
|
|
163
|
+
if (!trustedKeysOverrideLogged) {
|
|
164
|
+
log(`ORBIT_RELEASE_TRUSTED_KEYS_FILE=${TRUSTED_KEYS_OVERRIDE_PATH} set; trusting replacement release signing key set`);
|
|
165
|
+
trustedKeysOverrideLogged = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function readTrustedKeysManifest(manifestPath) {
|
|
170
|
+
const manifestDir = path.dirname(manifestPath);
|
|
171
|
+
return fs
|
|
172
|
+
.readFileSync(manifestPath, 'utf8')
|
|
173
|
+
.split(/\r?\n/)
|
|
174
|
+
.map((line) => line.trim())
|
|
175
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'))
|
|
176
|
+
.map((line) => {
|
|
177
|
+
const [id, notAfter, revokedAt, publicKeyPath, ...extra] = line.split('|');
|
|
178
|
+
if (!id || !publicKeyPath || extra.length > 0) {
|
|
179
|
+
throw new Error(`invalid trusted release signing key record: ${line}`);
|
|
180
|
+
}
|
|
181
|
+
const resolvedPublicKeyPath = path.isAbsolute(publicKeyPath)
|
|
182
|
+
? publicKeyPath
|
|
183
|
+
: path.resolve(manifestDir, publicKeyPath);
|
|
184
|
+
return {
|
|
185
|
+
id,
|
|
186
|
+
notAfter: notAfter || null,
|
|
187
|
+
revokedAt: revokedAt || null,
|
|
188
|
+
publicKeyPem: fs.readFileSync(resolvedPublicKeyPath, 'utf8'),
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeTrustedReleaseKeys(trustedKeys) {
|
|
194
|
+
if (typeof trustedKeys === 'string') {
|
|
195
|
+
return [
|
|
196
|
+
{
|
|
197
|
+
id: 'override',
|
|
198
|
+
notAfter: null,
|
|
199
|
+
revokedAt: null,
|
|
200
|
+
publicKeyPem: trustedKeys,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!Array.isArray(trustedKeys) || trustedKeys.length === 0) {
|
|
206
|
+
throw new Error('at least one trusted release signing key is required');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return trustedKeys.map((key) => {
|
|
210
|
+
if (!key || !key.id || !key.publicKeyPem) {
|
|
211
|
+
throw new Error('trusted release signing keys require id and publicKeyPem');
|
|
212
|
+
}
|
|
213
|
+
const notAfter = key.notAfter || null;
|
|
214
|
+
const revokedAt = key.revokedAt || null;
|
|
215
|
+
// Mirror the shell-side awk regex (release_date_number) so a malformed
|
|
216
|
+
// override like notAfter: "next month" fails closed instead of silently
|
|
217
|
+
// becoming "never expires" under lexicographic comparison.
|
|
218
|
+
if (notAfter !== null && !/^\d{4}-\d{2}-\d{2}$/.test(notAfter)) {
|
|
219
|
+
throw new Error(`trusted release signing key ${key.id} has invalid notAfter: ${notAfter} (expected YYYY-MM-DD)`);
|
|
220
|
+
}
|
|
221
|
+
if (revokedAt !== null && !/^\d{4}-\d{2}-\d{2}$/.test(revokedAt)) {
|
|
222
|
+
throw new Error(`trusted release signing key ${key.id} has invalid revokedAt: ${revokedAt} (expected YYYY-MM-DD)`);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
id: key.id,
|
|
226
|
+
notAfter,
|
|
227
|
+
revokedAt,
|
|
228
|
+
publicKeyPem: key.publicKeyPem,
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readTrustedReleaseKeys() {
|
|
234
|
+
acknowledgeTrustedPublicKeyOverride();
|
|
235
|
+
acknowledgeTrustedKeysOverride();
|
|
236
|
+
|
|
237
|
+
if (PUBLIC_KEY_OVERRIDE) {
|
|
238
|
+
return normalizeTrustedReleaseKeys(fs.readFileSync(TRUSTED_PUBLIC_KEY_PATH, 'utf8'));
|
|
239
|
+
}
|
|
240
|
+
if (TRUSTED_KEYS_OVERRIDE_PATH) {
|
|
241
|
+
return normalizeTrustedReleaseKeys(readTrustedKeysManifest(TRUSTED_KEYS_OVERRIDE_PATH));
|
|
242
|
+
}
|
|
243
|
+
return TRUSTED_RELEASE_KEYS;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function releaseDateString(date = new Date()) {
|
|
247
|
+
return date.toISOString().slice(0, 10);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function assertTrustedKeyUsable(key, verificationDate = new Date()) {
|
|
251
|
+
if (key.revokedAt) {
|
|
252
|
+
throw new Error(`release checksum signature was made by revoked release signing key ${key.id} (revoked ${key.revokedAt})`);
|
|
253
|
+
}
|
|
254
|
+
if (key.notAfter && releaseDateString(verificationDate) > key.notAfter) {
|
|
255
|
+
throw new Error(`release checksum signature was made by expired release signing key ${key.id} (not_after ${key.notAfter})`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function verifyWithPublicKey(checksumText, signatureBuf, publicKeyPem) {
|
|
260
|
+
const verifier = crypto.createVerify('RSA-SHA256');
|
|
261
|
+
verifier.update(checksumText, 'utf8');
|
|
262
|
+
verifier.end();
|
|
263
|
+
return verifier.verify(publicKeyPem, signatureBuf);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function verifyChecksumSignature(
|
|
267
|
+
checksumText,
|
|
268
|
+
signatureBuf,
|
|
269
|
+
trustedKeys = readTrustedReleaseKeys(),
|
|
270
|
+
verificationDate = new Date()
|
|
271
|
+
) {
|
|
272
|
+
for (const key of normalizeTrustedReleaseKeys(trustedKeys)) {
|
|
273
|
+
if (verifyWithPublicKey(checksumText, signatureBuf, key.publicKeyPem)) {
|
|
274
|
+
assertTrustedKeyUsable(key, verificationDate);
|
|
275
|
+
log(`authenticated ${CHECKSUM_FILE} with release signing key ${key.id}`);
|
|
276
|
+
return key.id;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`release checksum signature verification failed for ${CHECKSUM_FILE}: no trusted release signing key matched`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function verifyArchiveChecksum(asset, archiveBuf, checksumText) {
|
|
283
|
+
const checksums = parseChecksums(checksumText);
|
|
284
|
+
const expected = checksums[asset];
|
|
285
|
+
if (!expected) {
|
|
286
|
+
throw new Error(`checksum entry for ${asset} was not found in ${CHECKSUM_FILE}`);
|
|
287
|
+
}
|
|
288
|
+
const actual = sha256(archiveBuf);
|
|
289
|
+
if (actual !== expected) {
|
|
290
|
+
throw new Error(`checksum mismatch for ${asset}: expected ${expected}, got ${actual}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function runTar(args, options = {}) {
|
|
295
|
+
const result = spawnSync('tar', args, { encoding: 'utf8', ...options });
|
|
296
|
+
if (result.error) {
|
|
297
|
+
throw result.error;
|
|
298
|
+
}
|
|
83
299
|
if (result.status !== 0) {
|
|
84
|
-
|
|
300
|
+
const stderr = result.stderr ? `: ${result.stderr.trim()}` : '';
|
|
301
|
+
throw new Error(`tar ${args.join(' ')} failed (status ${result.status})${stderr}`);
|
|
302
|
+
}
|
|
303
|
+
return result.stdout || '';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function validateArchiveMemberName(member) {
|
|
307
|
+
if (member === 'orbit') {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!member || member.startsWith('/') || member.includes('..') || path.isAbsolute(member)) {
|
|
311
|
+
throw new Error(`unsafe release archive member: ${member || '<empty>'}`);
|
|
312
|
+
}
|
|
313
|
+
throw new Error(`unexpected release archive member: ${member}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function validateArchiveMembers(archivePath) {
|
|
317
|
+
const members = runTar(['-tzf', archivePath])
|
|
318
|
+
.split(/\r?\n/)
|
|
319
|
+
.filter((line) => line.length > 0);
|
|
320
|
+
if (members.length !== 1) {
|
|
321
|
+
throw new Error("release archive must contain only the 'orbit' binary");
|
|
322
|
+
}
|
|
323
|
+
validateArchiveMemberName(members[0]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function validateExtractedBinary(extractedBinary) {
|
|
327
|
+
const stat = fs.lstatSync(extractedBinary);
|
|
328
|
+
if (stat.isSymbolicLink()) {
|
|
329
|
+
throw new Error('release archive member is a symlink: orbit');
|
|
330
|
+
}
|
|
331
|
+
if (!stat.isFile()) {
|
|
332
|
+
throw new Error('release archive member must be a regular file: orbit');
|
|
333
|
+
}
|
|
334
|
+
if (stat.nlink !== 1) {
|
|
335
|
+
throw new Error('release archive member must not be a hard link: orbit');
|
|
85
336
|
}
|
|
86
337
|
}
|
|
87
338
|
|
|
339
|
+
function extractTarGz(archivePath, destDir) {
|
|
340
|
+
validateArchiveMembers(archivePath);
|
|
341
|
+
runTar(['-xzf', archivePath, '-C', destDir, 'orbit'], { stdio: 'inherit' });
|
|
342
|
+
validateExtractedBinary(path.join(destDir, 'orbit'));
|
|
343
|
+
}
|
|
344
|
+
|
|
88
345
|
async function main() {
|
|
89
346
|
if (process.env.ORBIT_SKIP_DOWNLOAD === '1') {
|
|
90
347
|
log('ORBIT_SKIP_DOWNLOAD=1 set; skipping binary download.');
|
|
@@ -99,12 +356,23 @@ async function main() {
|
|
|
99
356
|
const asset = `orbit-${target}.tar.gz`;
|
|
100
357
|
const baseUrl = `https://github.com/${BINARY_REPO}/releases/download/${BINARY_VERSION}`;
|
|
101
358
|
const archiveUrl = `${baseUrl}/${asset}`;
|
|
102
|
-
const checksumUrl = `${baseUrl}
|
|
359
|
+
const checksumUrl = `${baseUrl}/${CHECKSUM_FILE}`;
|
|
360
|
+
const checksumSignatureUrl = `${baseUrl}/${CHECKSUM_SIGNATURE_FILE}`;
|
|
103
361
|
|
|
104
362
|
log(`installing orbit ${BINARY_VERSION} for ${target}...`);
|
|
105
363
|
|
|
106
364
|
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
107
365
|
|
|
366
|
+
let checksumText;
|
|
367
|
+
let signatureBuf;
|
|
368
|
+
try {
|
|
369
|
+
checksumText = (await fetchBuffer(checksumUrl)).toString('utf8');
|
|
370
|
+
signatureBuf = await fetchBuffer(checksumSignatureUrl);
|
|
371
|
+
verifyChecksumSignature(checksumText, signatureBuf);
|
|
372
|
+
} catch (err) {
|
|
373
|
+
fail(`could not authenticate ${CHECKSUM_FILE}: ${err.message}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
108
376
|
let archiveBuf;
|
|
109
377
|
try {
|
|
110
378
|
archiveBuf = await fetchBuffer(archiveUrl);
|
|
@@ -113,35 +381,45 @@ async function main() {
|
|
|
113
381
|
}
|
|
114
382
|
|
|
115
383
|
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
|
-
}
|
|
384
|
+
verifyArchiveChecksum(asset, archiveBuf, checksumText);
|
|
127
385
|
} catch (err) {
|
|
128
|
-
fail(
|
|
386
|
+
fail(err.message);
|
|
129
387
|
}
|
|
130
388
|
|
|
131
389
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'orbit-cli-'));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
390
|
+
try {
|
|
391
|
+
const archivePath = path.join(tmpDir, asset);
|
|
392
|
+
fs.writeFileSync(archivePath, archiveBuf);
|
|
393
|
+
extractTarGz(archivePath, tmpDir);
|
|
135
394
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
395
|
+
const extractedBinary = path.join(tmpDir, 'orbit');
|
|
396
|
+
if (!fs.existsSync(extractedBinary)) {
|
|
397
|
+
fail(`extracted archive did not contain 'orbit' binary at ${extractedBinary}`);
|
|
398
|
+
}
|
|
399
|
+
fs.copyFileSync(extractedBinary, BIN_PATH);
|
|
400
|
+
fs.chmodSync(BIN_PATH, 0o755);
|
|
401
|
+
} finally {
|
|
402
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
139
403
|
}
|
|
140
|
-
fs.copyFileSync(extractedBinary, BIN_PATH);
|
|
141
|
-
fs.chmodSync(BIN_PATH, 0o755);
|
|
142
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
143
404
|
|
|
144
405
|
log(`installed orbit binary at ${BIN_PATH}`);
|
|
145
406
|
}
|
|
146
407
|
|
|
147
|
-
|
|
408
|
+
if (require.main === module) {
|
|
409
|
+
main().catch((err) => fail(err && err.message ? err.message : String(err)));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = {
|
|
413
|
+
TRUSTED_RELEASE_KEYS,
|
|
414
|
+
parseChecksums,
|
|
415
|
+
sha256,
|
|
416
|
+
acknowledgeTrustedPublicKeyOverride,
|
|
417
|
+
acknowledgeTrustedKeysOverride,
|
|
418
|
+
extractTarGz,
|
|
419
|
+
normalizeTrustedReleaseKeys,
|
|
420
|
+
readTrustedReleaseKeys,
|
|
421
|
+
validateArchiveMembers,
|
|
422
|
+
validateExtractedBinary,
|
|
423
|
+
verifyArchiveChecksum,
|
|
424
|
+
verifyChecksumSignature,
|
|
425
|
+
};
|