iterate-skill-installer 3.9.4 → 3.11.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 -5
  2. package/package.json +1 -1
package/lib/installer.js CHANGED
@@ -166,6 +166,25 @@ const ITERATE_SIGNING_PUBLIC_KEY = [
166
166
  '-----END PGP PUBLIC KEY BLOCK-----',
167
167
  ].join('\n');
168
168
 
169
+ //: SHA-1 fingerprint of the pinned signing key (uid jingzhao-l (sign-github)).
170
+ //: Verification is only accepted when the ``VALIDSIG`` fingerprint belongs to
171
+ //: this key, not merely on a GOODSIG/exit-0 — a substituted signer whose key
172
+ //: gpg auto-retrieved would otherwise pass. Keep in sync with
173
+ //: ``iterate_cli/signing.py`` and ``scripts/install.py``.
174
+ const ITERATE_SIGNING_KEY_FINGERPRINT = '0929EA31DF4F7429F63FC53189D88B1D043A1298';
175
+
176
+ //: Extract the fingerprints gpg reported as ``VALIDSIG`` in ``--status-fd``
177
+ //: output. A status line is ``[GNUPG:] VALIDSIG <fpr> <date> …``.
178
+ function validsigFingerprints(statusOutput) {
179
+ const fprs = [];
180
+ for (const line of String(statusOutput || '').split('\n')) {
181
+ if (!line.startsWith('[GNUPG:] VALIDSIG ')) continue;
182
+ const parts = line.split(/\s+/);
183
+ if (parts.length >= 3 && parts[2]) fprs.push(parts[2].toUpperCase());
184
+ }
185
+ return fprs;
186
+ }
187
+
169
188
  const ITERATE_BANNER = [
170
189
  '██╗████████╗███████╗██████╗ █████╗ ████████╗███████╗',
171
190
  '██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝',
@@ -593,7 +612,13 @@ function verifyGpgDetachedSignature(artifactPath, signaturePath, publicKeyArmor,
593
612
  return { status: 'unavailable', detail: 'gpg binary not found' };
594
613
  }
595
614
  if (importResult.error.code === 'ETIMEDOUT' || importResult.signal === 'SIGKILL') {
596
- return { status: 'unavailable', detail: `gpg import timed out after ${timeout} ms` };
615
+ // A hung gpg is not missing evidence: classify it 'invalid' so the
616
+ // ITERATE_ALLOW_UNSIGNED override cannot bypass authenticity (fail closed,
617
+ // mirroring the verify-timeout branch below and scripts/install.py).
618
+ return {
619
+ status: 'invalid',
620
+ detail: `gpg import timed out after ${timeout} ms (treated as invalid)`,
621
+ };
597
622
  }
598
623
  return { status: 'unavailable', detail: importResult.error.message };
599
624
  }
@@ -610,17 +635,30 @@ function verifyGpgDetachedSignature(artifactPath, signaturePath, publicKeyArmor,
610
635
 
611
636
  const verifyResult = spawnSync(
612
637
  'gpg',
613
- ['--homedir', gpgHome, '--batch', '--quiet', '--no-autostart', '--verify', signaturePath, artifactPath],
638
+ ['--homedir', gpgHome, '--batch', '--quiet', '--no-autostart', '--no-auto-key-retrieve', '--status-fd', '1', '--verify', signaturePath, artifactPath],
614
639
  { encoding: 'utf8', timeout, killSignal: 'SIGKILL' }
615
640
  );
616
641
  if (verifyResult.error) {
617
642
  if (verifyResult.error.code === 'ETIMEDOUT' || verifyResult.signal === 'SIGKILL') {
618
- return { status: 'unavailable', detail: `gpg verify timed out after ${timeout} ms` };
643
+ // A hung gpg is not a missing dependency: classify it as 'invalid' so the
644
+ // ITERATE_ALLOW_UNSIGNED override cannot bypass authenticity. Mirrors
645
+ // iterate_cli/signing.py and scripts/install.py (keep in sync).
646
+ return {
647
+ status: 'invalid',
648
+ detail: `gpg verify timed out after ${timeout} ms (treated as invalid)`,
649
+ };
619
650
  }
620
651
  return { status: 'unavailable', detail: `gpg launch failed: ${verifyResult.error.message}` };
621
652
  }
622
- // gpg exit 0 = good signature; 1 = bad signature; 2 = fatal (bad key usage).
623
- if (verifyResult.status === 0) {
653
+ // A GOODSIG (or exit 0) alone does not prove the signer is the pinned key: a
654
+ // substituted signer whose key gpg auto-retrieved would pass. Require a
655
+ // VALIDSIG fingerprint belonging to the pinned key, mirroring
656
+ // iterate_cli/signing.py and scripts/install.py (keep in sync).
657
+ const signerFingerprints = validsigFingerprints(verifyResult.stdout);
658
+ const matchesPinned = signerFingerprints.some(
659
+ (fpr) => fpr === ITERATE_SIGNING_KEY_FINGERPRINT
660
+ );
661
+ if (verifyResult.status === 0 && matchesPinned) {
624
662
  return { status: 'verified', detail: (verifyResult.stderr || '').trim() };
625
663
  }
626
664
  return { status: 'invalid', detail: (verifyResult.stderr || '').trim() || `gpg exited ${verifyResult.status}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iterate-skill-installer",
3
- "version": "3.9.4",
3
+ "version": "3.11.0",
4
4
  "description": "One-command installer for iterate-skill across AI coding assistants",
5
5
  "main": "lib/installer.js",
6
6
  "files": [