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.
Files changed (2) hide show
  1. package/lib/installer.js +43 -6
  2. 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
- // gpg exit 0 = good signature; 1 = bad signature; 2 = fatal (bad key usage).
635
- if (verifyResult.status === 0) {
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?.find((a) => a.name === TARBALL_ASSET_NAME);
1615
- const checksumAsset = release.assets?.find((a) => a.name === CHECKSUMS_ASSET_NAME);
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?.find((a) => a.name === TARBALL_SIGNATURE_ASSET_NAME);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-skill-installer",
3
- "version": "3.10.0",
3
+ "version": "3.12.0",
4
4
  "description": "One-command installer for iterate-skill across AI coding assistants",
5
5
  "main": "lib/installer.js",
6
6
  "files": [