iterate-skill-installer 3.10.0 → 3.12.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/lib/installer.js +43 -6
- package/package.json +1 -1
package/lib/installer.js
CHANGED
|
@@ -56,6 +56,16 @@ const CHECKSUMS_ASSET_NAME = 'SHA256SUMS.txt';
|
|
|
56
56
|
// verifies authenticity (not just integrity) against the embedded signing key.
|
|
57
57
|
const TARBALL_SIGNATURE_ASSET_NAME = 'iterate-skill.tar.gz.asc';
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Find a release asset by name, tolerating a malformed payload.
|
|
61
|
+
* Guards each element (a malformed GitHub API response can contain a null /
|
|
62
|
+
* non-object entry before the matching asset; `a.name` on null would throw a
|
|
63
|
+
* TypeError instead of the intended clean "release is missing …" error path).
|
|
64
|
+
*/
|
|
65
|
+
function findReleaseAsset(assets, name) {
|
|
66
|
+
return Array.isArray(assets) ? assets.find((a) => a && a.name === name) : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
// Hard cap on a single curl download: a misbehaving CDN or a corrupt release
|
|
60
70
|
// must never hang the installer forever.
|
|
61
71
|
const CURL_MAX_TIME_SECONDS = '120';
|
|
@@ -166,6 +176,25 @@ const ITERATE_SIGNING_PUBLIC_KEY = [
|
|
|
166
176
|
'-----END PGP PUBLIC KEY BLOCK-----',
|
|
167
177
|
].join('\n');
|
|
168
178
|
|
|
179
|
+
//: SHA-1 fingerprint of the pinned signing key (uid jingzhao-l (sign-github)).
|
|
180
|
+
//: Verification is only accepted when the ``VALIDSIG`` fingerprint belongs to
|
|
181
|
+
//: this key, not merely on a GOODSIG/exit-0 — a substituted signer whose key
|
|
182
|
+
//: gpg auto-retrieved would otherwise pass. Keep in sync with
|
|
183
|
+
//: ``iterate_cli/signing.py`` and ``scripts/install.py``.
|
|
184
|
+
const ITERATE_SIGNING_KEY_FINGERPRINT = '0929EA31DF4F7429F63FC53189D88B1D043A1298';
|
|
185
|
+
|
|
186
|
+
//: Extract the fingerprints gpg reported as ``VALIDSIG`` in ``--status-fd``
|
|
187
|
+
//: output. A status line is ``[GNUPG:] VALIDSIG <fpr> <date> …``.
|
|
188
|
+
function validsigFingerprints(statusOutput) {
|
|
189
|
+
const fprs = [];
|
|
190
|
+
for (const line of String(statusOutput || '').split('\n')) {
|
|
191
|
+
if (!line.startsWith('[GNUPG:] VALIDSIG ')) continue;
|
|
192
|
+
const parts = line.split(/\s+/);
|
|
193
|
+
if (parts.length >= 3 && parts[2]) fprs.push(parts[2].toUpperCase());
|
|
194
|
+
}
|
|
195
|
+
return fprs;
|
|
196
|
+
}
|
|
197
|
+
|
|
169
198
|
const ITERATE_BANNER = [
|
|
170
199
|
'██╗████████╗███████╗██████╗ █████╗ ████████╗███████╗',
|
|
171
200
|
'██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝',
|
|
@@ -616,7 +645,7 @@ function verifyGpgDetachedSignature(artifactPath, signaturePath, publicKeyArmor,
|
|
|
616
645
|
|
|
617
646
|
const verifyResult = spawnSync(
|
|
618
647
|
'gpg',
|
|
619
|
-
['--homedir', gpgHome, '--batch', '--quiet', '--no-autostart', '--verify', signaturePath, artifactPath],
|
|
648
|
+
['--homedir', gpgHome, '--batch', '--quiet', '--no-autostart', '--no-auto-key-retrieve', '--status-fd', '1', '--verify', signaturePath, artifactPath],
|
|
620
649
|
{ encoding: 'utf8', timeout, killSignal: 'SIGKILL' }
|
|
621
650
|
);
|
|
622
651
|
if (verifyResult.error) {
|
|
@@ -631,8 +660,15 @@ function verifyGpgDetachedSignature(artifactPath, signaturePath, publicKeyArmor,
|
|
|
631
660
|
}
|
|
632
661
|
return { status: 'unavailable', detail: `gpg launch failed: ${verifyResult.error.message}` };
|
|
633
662
|
}
|
|
634
|
-
//
|
|
635
|
-
|
|
663
|
+
// A GOODSIG (or exit 0) alone does not prove the signer is the pinned key: a
|
|
664
|
+
// substituted signer whose key gpg auto-retrieved would pass. Require a
|
|
665
|
+
// VALIDSIG fingerprint belonging to the pinned key, mirroring
|
|
666
|
+
// iterate_cli/signing.py and scripts/install.py (keep in sync).
|
|
667
|
+
const signerFingerprints = validsigFingerprints(verifyResult.stdout);
|
|
668
|
+
const matchesPinned = signerFingerprints.some(
|
|
669
|
+
(fpr) => fpr === ITERATE_SIGNING_KEY_FINGERPRINT
|
|
670
|
+
);
|
|
671
|
+
if (verifyResult.status === 0 && matchesPinned) {
|
|
636
672
|
return { status: 'verified', detail: (verifyResult.stderr || '').trim() };
|
|
637
673
|
}
|
|
638
674
|
return { status: 'invalid', detail: (verifyResult.stderr || '').trim() || `gpg exited ${verifyResult.status}` };
|
|
@@ -1611,11 +1647,11 @@ async function main(options = {}) {
|
|
|
1611
1647
|
}
|
|
1612
1648
|
success(`Latest release: ${tag}`);
|
|
1613
1649
|
|
|
1614
|
-
const tarballAsset = release.assets
|
|
1615
|
-
const checksumAsset = release.assets
|
|
1650
|
+
const tarballAsset = findReleaseAsset(release.assets, TARBALL_ASSET_NAME);
|
|
1651
|
+
const checksumAsset = findReleaseAsset(release.assets, CHECKSUMS_ASSET_NAME);
|
|
1616
1652
|
// Optional `.asc` signature; its absence only downgrades authenticity to
|
|
1617
1653
|
// integrity (see the best-effort verify below), never fails the install.
|
|
1618
|
-
const signatureAsset = release.assets
|
|
1654
|
+
const signatureAsset = findReleaseAsset(release.assets, TARBALL_SIGNATURE_ASSET_NAME);
|
|
1619
1655
|
|
|
1620
1656
|
if (!tarballAsset) {
|
|
1621
1657
|
error(`Release is missing the ${TARBALL_ASSET_NAME} asset.`);
|
|
@@ -1794,6 +1830,7 @@ module.exports = {
|
|
|
1794
1830
|
parseChecksums,
|
|
1795
1831
|
constantTimeHashEqual,
|
|
1796
1832
|
enforceDownloadedSize,
|
|
1833
|
+
findReleaseAsset,
|
|
1797
1834
|
extractTarball,
|
|
1798
1835
|
rejectLinkEntries,
|
|
1799
1836
|
sumDeclaredSizes,
|