OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / drive-boot / release-manifest.mjs

108 lines · javascript ref:
// release-manifest.mjs — the signed release manifest for drive-boot code drives.
//
// This is the trust anchor for the self-updating appliance (docs/DRIVE-BOOT-APPLIANCE.md,
// §4.2). It is the drive-boot analogue of the SSH-signed annotated git tag the fleet
// updater verifies (fleet/updater.sh verify_tag) and a direct port of peerit's web
// release-signing discipline (peerit/js/release-verify.js + scripts/sign-release.mjs),
// adapted for a hyperdrive shipping RUNNABLE app code rather than a static web bundle.
//
// A drive-boot loader must NOT run a byte it hasn't checked against this manifest:
//   1. the manifest is signed by an OFFLINE Ed25519 release key (supplied out-of-band,
//      never in the origin's CI env) — so a compromised *online* seeding key cannot
//      forge a release the loader will execute;
//   2. the signed message covers the integrity-critical fields only — the per-file
//      SHA-256 map (what code runs), the driveKey (which drive), the release version,
//      and the runtime contract (Node major + packages-only flag) the code targets;
//   3. cosmetic fields (builtAt, note) are excluded so the signed message is
//      reproducible from the file set alone.
//
// Verification is fail-closed: any missing/typo'd field, wrong key, bad signature, or
// content-hash mismatch throws. The loader pins the release key (from the image /
// store manifest) and passes it as expectedKey.

import { createHash, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify } from 'node:crypto'

export const RELEASE_ALG = 'Ed25519'
export const RELEASE_MSG_VERSION = 'blindspark-drive-boot-v1'

const HEX64 = /^[0-9a-f]{64}$/i
const HEX128 = /^[0-9a-f]{128}$/i
// DER wrappers to turn a raw 32-byte Ed25519 seed / public key into PKCS8 / SPKI,
// identical to peerit/scripts/sign-release.mjs so the two trust chains stay aligned.
const PKCS8_PREFIX = '302e020100300506032b657004220420'
const SPKI_PREFIX = '302a300506032b6570032100'

export function sha256Hex (buf) {
  return createHash('sha256').update(buf).digest('hex')
}

// Deterministic, key-sorted JSON so signer and loader hash identical bytes.
function stable (v) {
  if (v === null || typeof v !== 'object') return JSON.stringify(v === undefined ? null : v)
  if (Array.isArray(v)) return '[' + v.map(stable).join(',') + ']'
  return '{' + Object.keys(v).sort().map(k => JSON.stringify(k) + ':' + stable(v[k])).join(',') + '}'
}

// The exact string the offline key signs / the loader verifies.
export function driveReleaseSigningMessage (manifest) {
  const m = manifest || {}
  return RELEASE_MSG_VERSION + '\n' + stable({
    files: m.files || {},
    driveKey: String(m.driveKey || '').toLowerCase(),
    version: String(m.version || ''),
    runtime: m.runtime || {}
  })
}

// Derive the { privateKey, pubHex } from a 32-byte hex seed (matches crypto.js /
// sign-release.mjs: PKCS8-wrapped raw seed → last-32-bytes-of-SPKI public key).
export function pubKeyFromSeedHex (seedHex) {
  const seed = String(seedHex || '').trim().toLowerCase()
  if (!HEX64.test(seed)) throw new Error('drive-boot: release seed must be 32-byte (64 hex)')
  const priv = createPrivateKey({ key: Buffer.from(PKCS8_PREFIX + seed, 'hex'), format: 'der', type: 'pkcs8' })
  const pubHex = createPublicKey(priv).export({ format: 'der', type: 'spki' }).subarray(-32).toString('hex')
  return { priv, pubHex }
}

// Sign a manifest with the offline seed → { alg, key, sig, msgVersion }.
export function signDriveManifest (manifest, seedHex) {
  const { priv, pubHex } = pubKeyFromSeedHex(seedHex)
  const sig = edSign(null, Buffer.from(driveReleaseSigningMessage(manifest), 'utf8'), priv).toString('hex')
  return { alg: RELEASE_ALG, key: pubHex, sig, msgVersion: RELEASE_MSG_VERSION }
}

// Verify a { alg, key, sig } signature over a manifest against a pinned key.
// Throws with a specific reason on any failure; returns { ok:true, key } on success.
export function verifyDriveManifest ({ manifest, signature, expectedKey } = {}) {
  if (!manifest || typeof manifest !== 'object') throw new Error('drive-boot: no manifest')
  const sig = signature || {}
  if (sig.alg !== RELEASE_ALG) throw new Error('drive-boot: unsupported signature algorithm')
  const key = String(sig.key || '').toLowerCase()
  if (!HEX64.test(key)) throw new Error('drive-boot: signing key is not a 32-byte hex Ed25519 key')
  if (expectedKey && key !== String(expectedKey).toLowerCase()) {
    throw new Error('drive-boot: signed by an unexpected key (not the pinned release key)')
  }
  if (!HEX128.test(String(sig.sig || ''))) throw new Error('drive-boot: signature is not 64-byte hex')
  const pub = createPublicKey({ key: Buffer.from(SPKI_PREFIX + key, 'hex'), format: 'der', type: 'spki' })
  const ok = edVerify(null, Buffer.from(driveReleaseSigningMessage(manifest), 'utf8'), pub, Buffer.from(sig.sig, 'hex'))
  if (!ok) throw new Error('drive-boot: signature did not verify against the manifest')
  return { ok: true, key }
}

// Given the drive's ACTUAL reconstructed contents (Map path→Buffer), confirm every
// file the manifest pins matches its sha256. This is the check that turns "the
// signature is valid" into "the code on disk is exactly the code that was signed."
// Throws on the first mismatch/missing file; returns { ok, count }.
export function verifyDriveContents (manifest, actualFiles) {
  const files = (manifest && manifest.files) || {}
  for (const [path, expected] of Object.entries(files)) {
    const buf = actualFiles.get(path)
    if (!buf) throw new Error('drive-boot: manifest file missing from drive: ' + path)
    const got = sha256Hex(buf)
    if (got !== String(expected).toLowerCase()) {
      throw new Error('drive-boot: hash mismatch for ' + path + ' (expected ' + expected + ', got ' + got + ')')
    }
  }
  return { ok: true, count: Object.keys(files).length }
}