OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / verify-public-hive-gateway-ops-evidence.mjs

133 lines · javascript ref:
#!/usr/bin/env node

import { constants } from 'node:fs'
import { open } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { isUtf8 } from 'node:buffer'
import { isAbsolute, resolve } from 'node:path'
import {
  verifyPublicHiveGatewayOpsEvidence
} from './lib/public-hive-gateway-ops.mjs'

const args = parseArgs(process.argv.slice(2))
if (args.help) {
  console.log(`Usage:
  node scripts/verify-public-hive-gateway-ops-evidence.mjs \\
    --evidence <ops.json> --contract <operator.json> \\
    --release-manifest <manifest.json> --release-sha <verified-tag-commit> \\
    --relay <name> --expected-contract-sha256 <64-hex>

Verifies a fresh fleet-mode artifact against the canonical signed cohort and
prints only a public summary. It proves no independent timestamp, blind G2/G3,
or organizational control.`)
  process.exit(0)
}

const required = ['evidence', 'contract', 'releaseManifest', 'releaseSha', 'relay', 'expectedContractSha256']
for (const name of required) if (!args[name]) throw new Error(`--${kebab(name)} is required`)
const evidenceBytes = await readBounded(requiredAbsolutePath(args.evidence, '--evidence'), 'operator readiness evidence', 2 * 1024 * 1024)
const contractBytes = await readBounded(requiredAbsolutePath(args.contract, '--contract'), 'operator readiness contract', 256 * 1024)
const manifestBytes = await readBounded(requiredAbsolutePath(args.releaseManifest, '--release-manifest'), 'public gateway release manifest', 2 * 1024 * 1024)
const result = verifyPublicHiveGatewayOpsEvidence(
  parseJson(evidenceBytes, 'operator readiness evidence'),
  {
    contract: parseJson(contractBytes, 'operator readiness contract'),
    manifest: parseJson(manifestBytes, 'public gateway release manifest'),
    releaseSha: args.releaseSha,
    relay: args.relay,
    expectedContractSha256: args.expectedContractSha256,
    contractFileSha256: createHash('sha256').update(contractBytes).digest('hex'),
    releaseManifestSha256: createHash('sha256').update(manifestBytes).digest('hex')
  }
)
console.log(JSON.stringify(result, null, 2))

function parseArgs (argv) {
  const out = {}
  const allowed = new Set([
    'evidence', 'contract', 'release-manifest', 'release-sha', 'relay',
    'expected-contract-sha256', 'help'
  ])
  const seen = new Set()
  for (let index = 0; index < argv.length; index++) {
    const raw = argv[index]
    if (raw === '-h') {
      if (seen.has('help')) throw new Error('Duplicate option: --help')
      seen.add('help')
      out.help = true
      continue
    }
    if (!raw.startsWith('--')) throw new Error(`Unexpected argument: ${raw}`)
    const name = raw.slice(2)
    if (!allowed.has(name)) throw new Error(`Unknown option: --${name}`)
    if (seen.has(name)) throw new Error(`Duplicate option: --${name}`)
    seen.add(name)
    if (name === 'help') {
      out.help = true
      continue
    }
    const value = argv[++index]
    if (!value || value.startsWith('--')) throw new Error(`Missing value for --${name}`)
    out[camel(name)] = value
  }
  return out
}

async function readBounded (value, label, maximum) {
  const file = resolve(value)
  let handle
  try {
    if (typeof constants.O_NOFOLLOW !== 'number') throw new Error('safe no-follow opens are unavailable')
    handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW)
    const before = await handle.stat({ bigint: true })
    if (!before.isFile() || before.nlink !== 1n || before.size < 1n || before.size > BigInt(maximum)) {
      throw new Error(`${label} must be a bounded single-link regular file`)
    }
    const buffer = Buffer.allocUnsafe(maximum + 1)
    let length = 0
    while (length < buffer.length) {
      const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length)
      if (bytesRead === 0) break
      length += bytesRead
    }
    const after = await handle.stat({ bigint: true })
    if (length > maximum || BigInt(length) !== after.size || !sameSnapshot(before, after)) {
      throw new Error(`${label} changed while being read`)
    }
    return buffer.subarray(0, length)
  } catch (err) {
    if (err?.message?.startsWith(label)) throw err
    throw new Error(`${label} must be a readable non-symlink file`)
  } finally {
    await handle?.close()
  }
}

function parseJson (buffer, label) {
  if (!isUtf8(buffer)) throw new Error(`${label} must be UTF-8 JSON`)
  let value
  try { value = JSON.parse(buffer.toString('utf8')) } catch { throw new Error(`${label} must be valid JSON`) }
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must contain a JSON object`)
  return value
}

function requiredAbsolutePath (value, label) {
  if (typeof value !== 'string' || !isAbsolute(value) || value.length > 4096 || /[\r\n\0]/.test(value)) {
    throw new Error(`${label} must be a bounded absolute path`)
  }
  return value
}

function sameSnapshot (left, right) {
  return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
    left.nlink === right.nlink && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs
}

function camel (value) {
  return value.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
}

function kebab (value) {
  return value.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`)
}