iterate-skill-installer 3.10.0 → 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 +29 -3
  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
  '██║╚══██╔══╝██╔════╝██╔══██╗██╔══██╗╚══██╔══╝██╔════╝',
@@ -616,7 +635,7 @@ function verifyGpgDetachedSignature(artifactPath, signaturePath, publicKeyArmor,
616
635
 
617
636
  const verifyResult = spawnSync(
618
637
  'gpg',
619
- ['--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],
620
639
  { encoding: 'utf8', timeout, killSignal: 'SIGKILL' }
621
640
  );
622
641
  if (verifyResult.error) {
@@ -631,8 +650,15 @@ function verifyGpgDetachedSignature(artifactPath, signaturePath, publicKeyArmor,
631
650
  }
632
651
  return { status: 'unavailable', detail: `gpg launch failed: ${verifyResult.error.message}` };
633
652
  }
634
- // gpg exit 0 = good signature; 1 = bad signature; 2 = fatal (bad key usage).
635
- 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) {
636
662
  return { status: 'verified', detail: (verifyResult.stderr || '').trim() };
637
663
  }
638
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.10.0",
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": [