@trops/dash-core 0.1.601 → 0.1.602

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.
@@ -13232,6 +13232,51 @@ async function verifyBufferSignature({ bytes, signature, publicKey }) {
13232
13232
  return ed.verifyAsync(sigBytes, digest, pubBytes);
13233
13233
  }
13234
13234
 
13235
+ // --- Manifest body signature (Phase 5D, audit P1 #24) ---
13236
+ //
13237
+ // Mirror of dash-registry/src/lib/crypto.ts#signManifestBody /
13238
+ // verifyManifestSignature. The registry's `/download` response carries
13239
+ // `{manifest_signature, manifest_signature_keyid}` over the rest of
13240
+ // the body. The installer verifies before consuming `downloadUrl` so
13241
+ // a MITM that swaps any field is caught client-side.
13242
+ //
13243
+ // Both sides must canonicalize identically: strip the two signature
13244
+ // fields, sort remaining keys recursively, no whitespace.
13245
+
13246
+ const CURRENT_MANIFEST_SIGNATURE_KEYID = "v1";
13247
+
13248
+ function canonicalizeManifestBody(body) {
13249
+ if (!body || typeof body !== "object") {
13250
+ throw new Error("manifest body must be an object");
13251
+ }
13252
+ const stripped = { ...body };
13253
+ delete stripped.manifest_signature;
13254
+ delete stripped.manifest_signature_keyid;
13255
+ return canonicalJsonStringify(stripped);
13256
+ }
13257
+
13258
+ /**
13259
+ * Verify a manifest signature against the registry root public key.
13260
+ * Returns true/false; never throws on a bad signature — caller
13261
+ * decides what to do based on mode (off/warn/strict).
13262
+ *
13263
+ * @param {object} args
13264
+ * @param {object} args.body — full response body INCLUDING signature fields (ignored on canonicalize)
13265
+ * @param {string} args.signature — base64 Ed25519 signature
13266
+ * @param {string} args.registryRootPublicKey — base64
13267
+ * @returns {Promise<boolean>}
13268
+ */
13269
+ async function verifyManifestSignature({
13270
+ body,
13271
+ signature,
13272
+ registryRootPublicKey,
13273
+ }) {
13274
+ const message = new TextEncoder().encode(canonicalizeManifestBody(body));
13275
+ const sigBytes = base64ToBytes(signature);
13276
+ const rootPubBytes = base64ToBytes(registryRootPublicKey);
13277
+ return ed.verifyAsync(sigBytes, message, rootPubBytes);
13278
+ }
13279
+
13235
13280
  var publisherCrypto = {
13236
13281
  canonicalJsonStringify,
13237
13282
  computeFingerprint: computeFingerprint$1,
@@ -13239,8 +13284,16 @@ var publisherCrypto = {
13239
13284
  verifyPublisherCert: verifyPublisherCert$1,
13240
13285
  signBuffer,
13241
13286
  verifyBufferSignature,
13287
+ verifyManifestSignature,
13288
+ CURRENT_MANIFEST_SIGNATURE_KEYID,
13242
13289
  // internal helpers — exported for tests
13243
- _internal: { bytesToBase64, base64ToBytes, bytesToHex, sha256 },
13290
+ _internal: {
13291
+ bytesToBase64,
13292
+ base64ToBytes,
13293
+ bytesToHex,
13294
+ sha256,
13295
+ canonicalizeManifestBody,
13296
+ },
13244
13297
  };
13245
13298
 
13246
13299
  /**
@@ -13320,6 +13373,8 @@ function requireVerifyRegistryInstall () {
13320
13373
  const {
13321
13374
  verifyPublisherCert,
13322
13375
  verifyBufferSignature,
13376
+ verifyManifestSignature,
13377
+ CURRENT_MANIFEST_SIGNATURE_KEYID,
13323
13378
  } = publisherCrypto;
13324
13379
  const { getRegistryRootPublicKey } = registryRootPublicKey;
13325
13380
 
@@ -13486,8 +13541,83 @@ function requireVerifyRegistryInstall () {
13486
13541
  return { verified: true, reason: null, mode, warnings };
13487
13542
  }
13488
13543
 
13544
+ /**
13545
+ * Phase 5D (audit P1 #24): verify the signature on the /download
13546
+ * response BODY before the caller consumes downloadUrl / zipSignature /
13547
+ * publisherCert from it. Closes the MITM vector where a swapped
13548
+ * response could redirect the installer to a different ZIP signed by
13549
+ * an attacker-controlled (but still legitimate) publisher cert.
13550
+ *
13551
+ * Same off/warn/strict modes as the zip+cert verifier above — the
13552
+ * env var `DASH_REGISTRY_VERIFY_SIGNED_INSTALL` controls both.
13553
+ *
13554
+ * The signature is computed server-side over the canonical JSON of
13555
+ * the body minus the two signature fields themselves. Verification
13556
+ * here re-canonicalizes the same way and checks against the bundled
13557
+ * registry root public key.
13558
+ *
13559
+ * @param {object} args
13560
+ * @param {object} args.responseBody — the full parsed JSON body from /download
13561
+ * @returns {Promise<{verified, reason, mode, warnings}>}
13562
+ */
13563
+ async function verifyDownloadManifest({ responseBody }) {
13564
+ const mode = readMode();
13565
+ const warnings = [];
13566
+
13567
+ if (mode === "off") {
13568
+ return { verified: true, reason: null, mode, warnings };
13569
+ }
13570
+
13571
+ if (!responseBody || typeof responseBody !== "object") {
13572
+ return applyMode(mode, "MANIFEST_BODY_MISSING", warnings);
13573
+ }
13574
+
13575
+ const signature = responseBody.manifest_signature;
13576
+ const keyid = responseBody.manifest_signature_keyid;
13577
+
13578
+ // Unsigned case: legacy registry deployments that haven't been
13579
+ // upgraded yet won't include these fields. In `warn` mode we log
13580
+ // + proceed so the rollout doesn't break existing installs; in
13581
+ // `strict` mode we refuse.
13582
+ if (!signature || !keyid) {
13583
+ return applyMode(mode, "UNSIGNED_MANIFEST", warnings);
13584
+ }
13585
+
13586
+ // Today only one root key is bundled. When rotation lands, this
13587
+ // becomes a lookup over an array of trusted public keys keyed by
13588
+ // keyid; unknown keyid → fail closed.
13589
+ if (keyid !== CURRENT_MANIFEST_SIGNATURE_KEYID) {
13590
+ return applyMode(
13591
+ mode,
13592
+ `MANIFEST_SIGNATURE_UNKNOWN_KEYID: ${keyid}`,
13593
+ warnings,
13594
+ );
13595
+ }
13596
+
13597
+ let ok = false;
13598
+ try {
13599
+ ok = await verifyManifestSignature({
13600
+ body: responseBody,
13601
+ signature,
13602
+ registryRootPublicKey: getRegistryRootPublicKey(),
13603
+ });
13604
+ } catch (err) {
13605
+ return applyMode(
13606
+ mode,
13607
+ `MANIFEST_SIGNATURE_ERROR: ${err.message || err}`,
13608
+ warnings,
13609
+ );
13610
+ }
13611
+ if (!ok) {
13612
+ return applyMode(mode, "MANIFEST_SIGNATURE_INVALID", warnings);
13613
+ }
13614
+
13615
+ return { verified: true, reason: null, mode, warnings };
13616
+ }
13617
+
13489
13618
  verifyRegistryInstall = {
13490
13619
  verifyDownloadedPackage,
13620
+ verifyDownloadManifest,
13491
13621
  _readMode: readMode,
13492
13622
  };
13493
13623
  return verifyRegistryInstall;
@@ -14842,6 +14972,27 @@ var schedulerController_1 = schedulerController$2;
14842
14972
  if (jsonData.error) {
14843
14973
  throw new Error(`Download failed: ${jsonData.error}`);
14844
14974
  }
14975
+
14976
+ // Phase 5D (P1 #24): verify the manifest signature BEFORE
14977
+ // trusting any field in this response. A MITM that swapped
14978
+ // downloadUrl or any signing-metadata field gets caught here
14979
+ // because the signature won't verify against the bundled
14980
+ // registry root key. Mode is shared with the existing
14981
+ // zip+cert verifier — DASH_REGISTRY_VERIFY_SIGNED_INSTALL.
14982
+ const {
14983
+ verifyDownloadManifest,
14984
+ } = requireVerifyRegistryInstall();
14985
+ const manifestVerify = await verifyDownloadManifest({
14986
+ responseBody: jsonData,
14987
+ });
14988
+ if (!manifestVerify.verified && manifestVerify.mode === "warn") {
14989
+ console.warn(
14990
+ `[widgetRegistry] Installing with unverified download manifest ` +
14991
+ `for "${widgetName}" — reason: ${manifestVerify.reason}. Set ` +
14992
+ `DASH_REGISTRY_VERIFY_SIGNED_INSTALL=strict to refuse.`,
14993
+ );
14994
+ }
14995
+
14845
14996
  if (jsonData.downloadUrl) {
14846
14997
  const zipResponse = await fetch(jsonData.downloadUrl);
14847
14998
  if (!zipResponse.ok) {