OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / public-hive-gateway-evidence.test.js

335 lines · javascript ref:
import test from 'brittle'
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
  mkdtemp,
  link,
  readFile,
  rm,
  symlink,
  writeFile
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
  PUBLIC_HIVE_GATEWAY_PROBE_CHECKS,
  readAndVerifyPublicHiveGatewayEvidence,
  verifyPublicHiveGatewayEvidence
} from '../../scripts/lib/public-hive-gateway-evidence.mjs'
import {
  PUBLIC_HIVE_GATEWAY_FINITE_POLICY
} from '../../scripts/lib/public-hive-gateway-policy.mjs'

const RELEASE_TARGET = 'v1.2.3'
const RELEASE_SHA = 'a'.repeat(40)
const APP_KEY = 'a'.repeat(64)
const APP_LABEL = 'ikikikikikikikikikikikikikikikikikikikikikikikikikiy'
const APP_ORIGIN = `https://${APP_LABEL}.hive-canary.example`
const FINGERPRINT = Array(32).fill('AA').join(':')
const NOW = Date.now()

test('public gateway evidence verifier binds a live proof to the exact release', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-gateway-evidence-'))
  t.teardown(() => rm(dir, { recursive: true, force: true }))
  const file = path.join(dir, 'live.json')
  const contents = Buffer.from(JSON.stringify(validEvidence(), null, 2) + '\n')
  await writeFile(file, contents, { mode: 0o600 })

  const result = await readAndVerifyPublicHiveGatewayEvidence({
    file,
    releaseTarget: RELEASE_TARGET,
    releaseSha: RELEASE_SHA
  })

  t.is(result.status, 'verified')
  t.is(result.releaseTarget, RELEASE_TARGET)
  t.is(result.releaseSha, RELEASE_SHA)
  t.is(result.contentSha256, 'b'.repeat(64))
  t.is(result.tlsProtocol, 'TLSv1.3')
  t.is(result.mode, 'fleet')
  t.is(result.admissionProfile, 'frozen-public-admission-v1')
  t.is(result.physicalEnforcementRequired, true)
  t.is(result.connectAddress, '127.0.0.1')
  t.is(result.nginxSha256, 'c'.repeat(64))
  t.is(result.evidenceSha256, createHash('sha256').update(contents).digest('hex'))
  t.alike(Object.keys(result.checks), PUBLIC_HIVE_GATEWAY_PROBE_CHECKS)
})

test('public gateway evidence verifier rejects release drift and incomplete live checks', (t) => {
  const wrongRelease = validEvidence()
  wrongRelease.release.sha = 'c'.repeat(40)
  t.exception(() => verifyPublicHiveGatewayEvidence(wrongRelease, expected()), /release SHA/)

  for (const check of PUBLIC_HIVE_GATEWAY_PROBE_CHECKS) {
    const evidence = validEvidence()
    evidence.probe.checks[check] = false
    t.exception(() => verifyPublicHiveGatewayEvidence(evidence, expected()), new RegExp(`check ${check}`))
  }

  const noProbe = validEvidence()
  noProbe.probe = null
  t.exception(() => verifyPublicHiveGatewayEvidence(noProbe, expected()), /gateway live probe must be an object/)

  const oldTls = validEvidence()
  oldTls.probe.tlsProtocol = 'TLSv1.1'
  t.exception(() => verifyPublicHiveGatewayEvidence(oldTls, expected()), /TLS 1\.2 or TLS 1\.3/)

  const noFingerprint = validEvidence()
  noFingerprint.probe.peerFingerprint256 = null
  t.exception(() => verifyPublicHiveGatewayEvidence(noFingerprint, expected()), /certificate fingerprint/)

  const expiringCertificate = validEvidence()
  expiringCertificate.probe.peerValidTo = new Date(NOW + 6 * 24 * 60 * 60 * 1000).toUTCString()
  t.exception(() => verifyPublicHiveGatewayEvidence(expiringCertificate, expected()), /remain valid for at least 7 days/)

  const noHash = validEvidence()
  noHash.probe.sha256 = ''
  t.exception(() => verifyPublicHiveGatewayEvidence(noHash, expected()), /content SHA-256/)

  const unsafeDriveVersion = validEvidence()
  unsafeDriveVersion.probe.driveVersion = String(Number.MAX_SAFE_INTEGER + 1)
  t.exception(() => verifyPublicHiveGatewayEvidence(unsafeDriveVersion, expected()), /safe integer string/)

  const secret = validEvidence()
  secret.static.warnings = ['HIVERELAY_API_KEY=should-never-enter-public-evidence']
  t.exception(() => verifyPublicHiveGatewayEvidence(secret, expected()), /must not contain API key/)

  const unknownCheck = validEvidence()
  unknownCheck.probe.checks.futureUnreviewedCheck = true
  t.exception(() => verifyPublicHiveGatewayEvidence(unknownCheck, expected()), /checks has unsupported fields/)

  const canaryPosture = validEvidence()
  canaryPosture.mode = 'canary'
  t.exception(() => verifyPublicHiveGatewayEvidence(canaryPosture, expected()), /explicit fleet production posture/)

  const transitionalAdmission = validEvidence()
  transitionalAdmission.admissionProfile = 'transitional-operator-allowlist-v1'
  t.exception(() => verifyPublicHiveGatewayEvidence(transitionalAdmission, expected()), /must not use transitional/)

  const wrongAppOrigin = validEvidence()
  wrongAppOrigin.probe.origin = `https://${'y'.repeat(52)}.hive-canary.example`
  t.exception(() => verifyPublicHiveGatewayEvidence(wrongAppOrigin, expected()), /origin hostname must exactly encode appKey/)

  const wrongSuffixBinding = validEvidence()
  wrongSuffixBinding.config.suffix = 'other-canary.example'
  t.exception(() => verifyPublicHiveGatewayEvidence(wrongSuffixBinding, expected()), /origin hostname must exactly encode appKey/)

  const missingConnectAddress = validEvidence()
  missingConnectAddress.probe.connectAddress = null
  t.exception(() => verifyPublicHiveGatewayEvidence(missingConnectAddress, expected()), /connectAddress must be an explicit IP address/)

  const wrongNodeBinding = validEvidence()
  wrongNodeBinding.probe.connectAddress = '127.0.0.2'
  t.exception(() => verifyPublicHiveGatewayEvidence(wrongNodeBinding, expected()), /connectAddress binding/)

  const noActiveNginx = validEvidence()
  noActiveNginx.nginx = null
  t.exception(() => verifyPublicHiveGatewayEvidence(noActiveNginx, expected()), /active nginx result must be an object/)

  const renderedNginx = validEvidence()
  renderedNginx.nginx.source = 'rendered'
  t.exception(() => verifyPublicHiveGatewayEvidence(renderedNginx, expected()), /nginx evidence source/)

  const unhashedNginx = validEvidence()
  unhashedNginx.nginx.sha256 = ''
  t.exception(() => verifyPublicHiveGatewayEvidence(unhashedNginx, expected()), /active nginx SHA-256/)

  const unsafeMultiAppSuffix = validEvidence()
  unsafeMultiAppSuffix.config.appKeyCount = 2
  t.exception(() => verifyPublicHiveGatewayEvidence(unsafeMultiAppSuffix, expected()), /multi-app fleet evidence requires Public Suffix/)

  const unboundedPolicy = validEvidence()
  unboundedPolicy.config.finiteProductionPolicy.maxResponseBytes = null
  t.exception(() => verifyPublicHiveGatewayEvidence(unboundedPolicy, expected()), /finite production policy maxResponseBytes/)

  const expandedEgress = validEvidence()
  expandedEgress.config.finiteProductionPolicy.egressBytesPerClientAppWindow++
  t.exception(() => verifyPublicHiveGatewayEvidence(expandedEgress, expected()), /finite production policy egressBytesPerClientAppWindow/)

  const oldSchema = validEvidence()
  oldSchema.schema = 'hiverelay-public-gateway-preflight-v1'
  t.exception(() => verifyPublicHiveGatewayEvidence(oldSchema, expected()), /gateway evidence schema/)

  const missingPhysicalRequirement = validEvidence()
  delete missingPhysicalRequirement.config.physicalEnforcementRequired
  t.exception(() => verifyPublicHiveGatewayEvidence(missingPhysicalRequirement, expected()), /physical enforcement requirement/)

  const disabledPhysicalRequirement = validEvidence()
  disabledPhysicalRequirement.config.physicalEnforcementRequired = false
  t.exception(() => verifyPublicHiveGatewayEvidence(disabledPhysicalRequirement, expected()), /physical enforcement requirement/)

  const stale = validEvidence()
  stale.checkedAt = new Date(NOW - 24 * 60 * 60 * 1000 - 1).toISOString()
  stale.probe.observedAt = new Date(NOW - 24 * 60 * 60 * 1000 - 2).toISOString()
  t.exception(() => verifyPublicHiveGatewayEvidence(stale, expected()), /older than the allowed evidence age/)
  t.is(verifyPublicHiveGatewayEvidence(stale, {
    ...expected(),
    maxAgeMs: 25 * 60 * 60 * 1000
  }).status, 'verified', 'tests and offline audits can inject an explicit evidence window')

  const future = validEvidence()
  future.checkedAt = new Date(NOW + 5 * 60 * 1000 + 1).toISOString()
  future.probe.observedAt = new Date(NOW + 5 * 60 * 1000).toISOString()
  t.exception(() => verifyPublicHiveGatewayEvidence(future, expected()), /more than 5 minutes in the future/)
})

test('public gateway evidence verifier refuses symlinks, hard links, and oversized files', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-gateway-evidence-'))
  t.teardown(() => rm(dir, { recursive: true, force: true }))
  const real = path.join(dir, 'real.json')
  const symlinkPath = path.join(dir, 'link.json')
  const hardLink = path.join(dir, 'hard-link.json')
  const large = path.join(dir, 'large.json')
  await writeFile(real, JSON.stringify(validEvidence()))
  await symlink(real, symlinkPath)
  await link(real, hardLink)
  await writeFile(large, Buffer.alloc(2 * 1024 * 1024 + 1, 0x20))

  await t.exception(readAndVerifyPublicHiveGatewayEvidence({ file: symlinkPath, ...expected() }), /non-symlink regular file/)
  await t.exception(readAndVerifyPublicHiveGatewayEvidence({ file: hardLink, ...expected() }), /one link/)
  await t.exception(readAndVerifyPublicHiveGatewayEvidence({ file: large, ...expected() }), /no larger than/)
})

test('public gateway evidence CLI emits only a public verification summary or digest', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-gateway-evidence-'))
  t.teardown(() => rm(dir, { recursive: true, force: true }))
  const file = path.join(dir, 'live.json')
  await writeFile(file, JSON.stringify(validEvidence()))
  const base = [
    'scripts/verify-public-hive-gateway-evidence.mjs',
    '--evidence', file,
    '--release-target', RELEASE_TARGET,
    '--release-sha', RELEASE_SHA,
    '--require-mode', 'fleet',
    '--require-admission-profile', 'frozen-public-admission-v1',
    '--expected-origin', APP_ORIGIN,
    '--expected-connect-address', '127.0.0.1',
    '--expected-app-key', APP_KEY,
    '--expected-path', '/index.html',
    '--expected-sha256', 'b'.repeat(64),
    '--expected-drive-version', '1',
    '--expected-peer-fingerprint256', FINGERPRINT,
    '--expected-nginx-sha256', 'c'.repeat(64)
  ]

  const summary = spawnSync(process.execPath, base, { encoding: 'utf8' })
  t.is(summary.status, 0, summary.stderr)
  const report = JSON.parse(summary.stdout)
  t.is(report.status, 'verified')
  t.is(report.origin, APP_ORIGIN)
  t.is(report.connectAddress, '127.0.0.1')
  t.is(report.nginxSha256, 'c'.repeat(64))
  t.is(report.physicalEnforcementRequired, true)
  t.is(report.path, '/index.html', 'public summary retains the bound content path')
  t.absent(summary.stdout.includes(file), 'private node-local evidence path is not printed')
  t.absent(summary.stdout.includes('apiKey'))

  const digest = spawnSync(process.execPath, [...base, '--digest-only'], { encoding: 'utf8' })
  t.is(digest.status, 0, digest.stderr)
  t.ok(/^[a-f0-9]{64}\n$/.test(digest.stdout))

  const token = spawnSync(process.execPath, [...base, '--rollout-token'], { encoding: 'utf8' })
  t.is(token.status, 0, token.stderr)
  t.ok(/^[A-Za-z0-9_-]+\n$/.test(token.stdout))
  const tokenBody = JSON.parse(Buffer.from(token.stdout.trim(), 'base64url').toString('utf8'))
  t.is(tokenBody.schema, 'hiverelay-public-gateway-evidence-verification-v2')
  t.is(tokenBody.physicalEnforcementRequired, true)
  t.is(tokenBody.evidenceSha256, digest.stdout.trim())
  t.is(tokenBody.origin, APP_ORIGIN)
  t.is(tokenBody.peerFingerprint256, FINGERPRINT)
  t.absent(JSON.stringify(tokenBody).includes(file), 'rollout token excludes the private evidence file path')

  const conflictingOutput = spawnSync(process.execPath, [...base, '--digest-only', '--rollout-token'], { encoding: 'utf8' })
  t.not(conflictingOutput.status, 0)
  t.ok(conflictingOutput.stderr.includes('mutually exclusive'))
})

test('public gateway preflight release binding flags are paired and public-safe', async (t) => {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-gateway-evidence-'))
  t.teardown(() => rm(dir, { recursive: true, force: true }))
  const output = path.join(dir, 'preflight.json')
  const script = 'scripts/preflight-public-hive-gateway.mjs'
  const config = 'deploy/public-hive-gateway/hiverelay-config.example.json'
  const env = { ...process.env, HIVERELAY_API_KEY: 'present-but-never-persisted' }

  const missingSha = spawnSync(process.execPath, [script, '--config', config, '--release-target', RELEASE_TARGET], {
    encoding: 'utf8',
    env
  })
  t.not(missingSha.status, 0)
  t.ok(missingSha.stderr.includes('must be provided together'))

  const bound = spawnSync(process.execPath, [
    script,
    '--config', config,
    '--release-target', RELEASE_TARGET,
    '--release-sha', RELEASE_SHA.toUpperCase(),
    '--evidence', output
  ], { encoding: 'utf8', env })
  t.is(bound.status, 0, bound.stderr)
  const evidence = JSON.parse(await readFile(output, 'utf8'))
  t.is(evidence.schema, 'hiverelay-public-gateway-preflight-v2')
  t.alike(evidence.release, { target: RELEASE_TARGET, sha: RELEASE_SHA })
  t.is(evidence.config.physicalEnforcementRequired, true)
  t.absent(JSON.stringify(evidence).includes(env.HIVERELAY_API_KEY))
})

function expected () {
  return { releaseTarget: RELEASE_TARGET, releaseSha: RELEASE_SHA, now: NOW }
}

function validEvidence (now = NOW) {
  return {
    schema: 'hiverelay-public-gateway-preflight-v2',
    status: 'pass',
    checkedAt: new Date(now - 1000).toISOString(),
    mode: 'fleet',
    admissionProfile: 'frozen-public-admission-v1',
    release: {
      target: RELEASE_TARGET,
      sha: RELEASE_SHA
    },
    config: {
      suffix: 'hive-canary.example',
      appKeyCount: 1,
      apiHost: '127.0.0.1',
      apiPort: 9100,
      gatewayHost: '127.0.0.1',
      gatewayPort: 9200,
      connectAddress: '127.0.0.1',
      publicSuffixReady: false,
      custodyEnabled: false,
      physicalEnforcementRequired: true,
      finiteProductionPolicy: { ...PUBLIC_HIVE_GATEWAY_FINITE_POLICY }
    },
    static: {
      ok: true,
      errors: [],
      warnings: ['public-safe warning']
    },
    nginx: {
      ok: true,
      errors: [],
      source: 'active',
      sha256: 'c'.repeat(64)
    },
    probe: {
      schema: 'hiverelay-public-gateway-probe-v1',
      observedAt: new Date(now - 2000).toISOString(),
      origin: APP_ORIGIN,
      connectAddress: '127.0.0.1',
      appKey: APP_KEY,
      path: '/index.html',
      sha256: 'b'.repeat(64),
      bytes: 42,
      driveVersion: '1',
      tlsProtocol: 'TLSv1.3',
      peerFingerprint256: FINGERPRINT,
      peerValidTo: new Date(now + 365 * 24 * 60 * 60 * 1000).toUTCString(),
      metadataSigned: false,
      checks: Object.fromEntries(PUBLIC_HIVE_GATEWAY_PROBE_CHECKS.map(name => [name, true]))
    },
    probeError: null
  }
}