OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / fleet-updater-install.test.js

227 lines · javascript ref:
import test from 'brittle'
import {
  chmodSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  realpathSync,
  rmSync,
  statSync,
  symlinkSync,
  writeFileSync
} from 'node:fs'
import { spawnSync } from 'node:child_process'
import { tmpdir } from 'node:os'
import path from 'node:path'

const repoRoot = path.resolve('.')
const installer = path.join(repoRoot, 'fleet', 'install-updater.sh')
const launcher = path.join(repoRoot, 'fleet', 'updater-launcher.sh')
const quarantineHelper = path.join(repoRoot, 'fleet', 'quarantine-public-gateway.sh')

function have (bin) {
  return spawnSync('sh', ['-c', `command -v ${bin}`], { stdio: 'ignore' }).status === 0
}

function runInstaller (destdir, channel = 'canary', relay = 'utah') {
  return spawnSync('bash', [installer, channel, relay], {
    encoding: 'utf8',
    env: {
      ...process.env,
      DESTDIR: destdir,
      HIVERELAY_REPO_DIR: repoRoot
    }
  })
}

test('updater installer is idempotent and preserves its root-only environment', (t) => {
  const root = mkdtempSync(path.join(tmpdir(), 'hr-updater-install-'))
  t.teardown(() => rmSync(root, { recursive: true, force: true }))

  const envDir = path.join(root, 'etc', 'hiverelay')
  const envFile = path.join(envDir, 'hiverelay-updater.env')
  const original = [
    'HIVERELAY_PUBLIC_GATEWAY_PROBE_CONFIG=/root/.hiverelay/config.json',
    'HIVERELAY_PUBLIC_GATEWAY_PROBE_NGINX_CONFIG=/etc/nginx/conf.d/hiverelay-public-apps.conf',
    'HIVERELAY_PUBLIC_GATEWAY_PROBE_EVIDENCE=/root/.hiverelay/evidence.json',
    'HIVERELAY_PUBLIC_GATEWAY_OPS_CERTIFICATE=/etc/letsencrypt/live/hive/fullchain.pem',
    'HIVERELAY_PUBLIC_GATEWAY_OPS_CERTIFICATE_KEY=/etc/letsencrypt/live/hive/privkey.pem',
    'HIVERELAY_PUBLIC_GATEWAY_OPS_CERTIFICATE_ROOT=/etc/letsencrypt',
    'HIVERELAY_PUBLIC_GATEWAY_OPS_SS_BINARY=/usr/sbin/ss',
    'HIVERELAY_PUBLIC_GATEWAY_OPS_EVIDENCE=/root/.hiverelay/ops-evidence.json',
    ''
  ].join('\n')
  mkdirSync(envDir, { recursive: true })
  writeFileSync(envFile, original)
  chmodSync(envFile, 0o600)

  const first = runInstaller(root, 'canary', 'utah')
  t.is(first.status, 0, first.stderr || first.stdout)
  t.is(readFileSync(envFile, 'utf8'), original, 'install preserves every operator setting byte')
  t.is(statSync(envFile).mode & 0o777, 0o600, 'environment is root-only')
  t.is(
    readFileSync(path.join(root, 'usr', 'local', 'bin', 'hiverelay-updater'), 'utf8'),
    readFileSync(launcher, 'utf8'),
    'installed entry point is the stable trust-checking launcher'
  )
  t.is(
    readFileSync(path.join(root, 'usr', 'local', 'sbin', 'hiverelay-quarantine-public-gateway'), 'utf8'),
    readFileSync(quarantineHelper, 'utf8'),
    'installer deploys the narrow public-edge quarantine helper'
  )
  t.is(
    statSync(path.join(root, 'usr', 'local', 'sbin', 'hiverelay-quarantine-public-gateway')).mode & 0o777,
    0o755
  )
  t.alike(
    readFileSync(path.join(root, 'etc', 'hiverelay-updater.conf'), 'utf8').trim().split('\n'),
    ['CHANNEL=canary', 'RELAY_NAME=utah', `REPO_DIR=${repoRoot}`]
  )

  const second = runInstaller(root, 'stable', 'sing-1')
  t.is(second.status, 0, second.stderr || second.stdout)
  t.is(readFileSync(envFile, 'utf8'), original, 'reinstall still preserves environment bytes')
  t.ok(readFileSync(path.join(root, 'etc', 'hiverelay-updater.conf'), 'utf8').startsWith('CHANNEL=stable\n'))
  t.ok(readFileSync(path.join(root, 'etc', 'hiverelay-updater.conf'), 'utf8').includes('RELAY_NAME=sing-1\n'))
  t.ok(second.stdout.includes('skipped systemctl'), 'DESTDIR regression path never calls host systemd')
})

test('updater installer requires a bounded canonical relay identity', (t) => {
  const root = mkdtempSync(path.join(tmpdir(), 'hr-updater-install-identity-'))
  t.teardown(() => rmSync(root, { recursive: true, force: true }))

  const missing = runInstaller(root, 'stable', '')
  t.not(missing.status, 0)
  t.ok(missing.stderr.includes('Invalid or missing relay name'))
  t.absent(existsSync(path.join(root, 'etc', 'hiverelay-updater.conf')))

  const malformed = runInstaller(root, 'stable', 'bad relay')
  t.not(malformed.status, 0)
  t.ok(malformed.stderr.includes('Invalid or missing relay name'))
  t.absent(existsSync(path.join(root, 'etc', 'hiverelay-updater.conf')))

  const wrongChannel = runInstaller(root, 'stable', 'utah')
  t.not(wrongChannel.status, 0)
  t.ok(wrongChannel.stderr.includes("not uniquely assigned to channel 'stable'"))
  t.absent(existsSync(path.join(root, 'etc', 'hiverelay-updater.conf')))
})

test('updater installer quarantines an environment symlink without touching its target', (t) => {
  const root = mkdtempSync(path.join(tmpdir(), 'hr-updater-install-link-'))
  t.teardown(() => rmSync(root, { recursive: true, force: true }))

  const envDir = path.join(root, 'etc', 'hiverelay')
  const envFile = path.join(envDir, 'hiverelay-updater.env')
  const sentinel = path.join(root, 'sentinel')
  mkdirSync(envDir, { recursive: true })
  writeFileSync(sentinel, 'do-not-touch\n')
  symlinkSync(sentinel, envFile)

  const result = runInstaller(root)
  t.is(result.status, 0, result.stderr || result.stdout)
  t.ok(result.stderr.includes('quarantined unsafe updater environment path'))
  t.is(readFileSync(sentinel, 'utf8'), 'do-not-touch\n')
  t.is(readFileSync(envFile, 'utf8'), '')
  t.is(statSync(envFile).mode & 0o777, 0o600)
})

const canSign = have('git') && have('ssh-keygen')

test('updater launcher runs exact trusted-tag code and rejects checkout drift', { skip: !canSign }, (t) => {
  const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'hr-updater-launcher-')))
  t.teardown(() => rmSync(root, { recursive: true, force: true }))

  const repo = path.join(root, 'repo')
  const fleet = path.join(repo, 'fleet')
  const key = path.join(root, 'release-key')
  const allowedSigners = path.join(root, 'allowed-signers')
  const updaterConf = path.join(root, 'hiverelay-updater.conf')
  const updaterEnv = path.join(root, 'hiverelay-updater.env')
  const marker = path.join(root, 'marker')
  mkdirSync(fleet, { recursive: true })
  writeFileSync(path.join(fleet, 'updater.sh'), '#!/bin/bash\nprintf \'%s\\n\' "$*" > "$MARKER"\n')

  const command = (bin, args, opts = {}) => {
    const result = spawnSync(bin, args, { encoding: 'utf8', ...opts })
    if (result.status !== 0 && opts.allowFail !== true) {
      t.fail(`${bin} ${args.join(' ')} failed: ${result.stderr || result.stdout}`)
    }
    return result
  }
  const git = (args, opts) => command('git', ['-C', repo, ...args], opts)

  command('ssh-keygen', ['-t', 'ed25519', '-f', key, '-N', '', '-q', '-C', 'release@hiverelay'])
  writeFileSync(allowedSigners, `release@hiverelay ${readFileSync(key + '.pub', 'utf8').trim()}\n`)
  writeFileSync(updaterConf, `REPO_DIR=${repo}\n`)
  writeFileSync(updaterEnv, '')
  chmodSync(updaterEnv, 0o600)
  command('git', ['init', '-q', repo])
  git(['config', 'user.name', 'tester'])
  git(['config', 'user.email', 'release@hiverelay'])
  git(['config', 'gpg.format', 'ssh'])
  git(['add', 'fleet/updater.sh'])
  git(['commit', '-qm', 'trusted updater'])
  git(['-c', `user.signingkey=${key}.pub`, 'tag', '-s', '-m', 'release', 'v1.2.3'])

  const run = (extraEnv = {}) => spawnSync('bash', [launcher, 'alpha', 'beta'], {
    encoding: 'utf8',
    env: {
      ...process.env,
      HIVERELAY_REPO_DIR: repo,
      HIVERELAY_ALLOWED_SIGNERS: allowedSigners,
      HIVERELAY_UPDATER_CONF: updaterConf,
      HIVERELAY_UPDATER_ENV_TRUST_FILE: updaterEnv,
      MARKER: marker,
      ...extraEnv
    }
  })

  const trusted = run()
  t.is(trusted.status, 0, trusted.stderr || trusted.stdout)
  t.is(readFileSync(marker, 'utf8'), 'alpha beta\n')

  const ancestorLink = path.join(root, 'repo-ancestor-link')
  symlinkSync(root, ancestorLink)
  const linkedAncestor = run({ HIVERELAY_REPO_DIR: path.join(ancestorLink, 'repo') })
  t.not(linkedAncestor.status, 0)
  t.ok(linkedAncestor.stderr.includes('path must be canonical and contain no symlink ancestors'))

  for (const line of [
    'HIVERELAY_HEALTH_TIMEOUT=1+2',
    'HIVERELAY_SERVICE=--no-block',
    'HIVERELAY_API=http://attacker.invalid:9100',
    'HIVERELAY_API=http://127.0.0.1:9100/path',
    'HIVERELAY_CONTROL_BRANCH=--upload-pack',
    'HIVERELAY_CONTROL_STATE=relative/state.json',
    'HIVERELAY_REQUIRE_SIGNED_TAGS=true',
    'HIVERELAY_PUBLIC_GATEWAY_PROBE_PUBLIC_SUFFIX_READY=2',
    'HIVERELAY_PUBLIC_GATEWAY_PROBE_CONFIG=/safe/../swap.json',
    'HIVERELAY_UNKNOWN_ROOT_OPTION=value'
  ]) {
    writeFileSync(updaterEnv, `${line}\n`)
    chmodSync(updaterEnv, 0o600)
    const hostile = run()
    t.not(hostile.status, 0, line)
    t.ok(hostile.stderr.includes('not canonical or in range') || hostile.stderr.includes('is not allowed'), line)
  }
  writeFileSync(updaterEnv, '')
  chmodSync(updaterEnv, 0o600)

  rmSync(marker)
  writeFileSync(path.join(fleet, 'updater.sh'), '#!/bin/bash\nprintf attack > "$MARKER"\n')
  const drifted = run()
  t.not(drifted.status, 0)
  t.ok(drifted.stderr.includes('differs from the signed checkout'))
  t.absent(existsSync(marker), 'modified updater is never executed')

  const missingTrust = run({ HIVERELAY_ALLOWED_SIGNERS: path.join(root, 'missing') })
  t.not(missingTrust.status, 0)

  chmodSync(root, 0o777)
  const unsafeAncestor = run()
  t.not(unsafeAncestor.status, 0)
  t.ok(unsafeAncestor.stderr.includes('parent must not be group/world writable'))
  chmodSync(root, 0o700)
})