hololake-system-architecture/product-source/hololake-native-desktop/server/release-broadcast/operator.mjs

431 lines
20 KiB
JavaScript
Executable file

#!/usr/bin/env node
import crypto from 'node:crypto'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { DEFAULT_PUBLIC_PREFIX, isMainModule, loadRuntimeState, normalizePublicPrefix } from './server.mjs'
const MAX_CONTROL_BYTES = 1024 * 1024
const SHA256_PATTERN = /^[a-f0-9]{64}$/
const SOURCE_COMMIT_PATTERN = /^[a-f0-9]{40}$/
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
const fail = (code) => {
throw new Error(code)
}
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex')
const requireText = (value, code) => {
if (typeof value !== 'string' || value.trim() === '') fail(code)
return value.trim()
}
function requireTrustedDirectory(directory, { enforceRootOwner = false } = {}) {
const info = fs.lstatSync(directory)
if (!info.isDirectory() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_INVALID')
if ((info.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_WRITABLE_BY_NON_OWNER')
if (enforceRootOwner && info.uid !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_DIRECTORY_NOT_ROOT_OWNED')
return info
}
function requireTrustedFile(file, { enforceRootOwner = false, maxBytes = null } = {}) {
const info = fs.lstatSync(file)
if (!info.isFile() || info.isSymbolicLink()) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_INVALID')
if ((info.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_WRITABLE_BY_NON_OWNER')
if (enforceRootOwner && info.uid !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_FILE_NOT_ROOT_OWNED')
if (maxBytes !== null && info.size > maxBytes) fail('HOLOLAKE_RELEASE_OPERATOR_CONTROL_FILE_TOO_LARGE')
return info
}
function readJson(file, { enforceRootOwner = false } = {}) {
requireTrustedFile(file, { enforceRootOwner, maxBytes: MAX_CONTROL_BYTES })
const bytes = fs.readFileSync(file)
return { bytes, value: JSON.parse(bytes.toString('utf8')) }
}
function directFile(directory, name, code) {
const value = requireText(name, code)
if (value !== path.basename(value) || value === '.' || value === '..' || value.includes('\0')) fail(code)
const resolved = path.resolve(directory, value)
if (path.dirname(resolved) !== directory) fail(code)
return resolved
}
function packageNameFromUrl(value, publicPrefix) {
const url = new URL(requireText(value, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_REQUIRED'))
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID')
}
let packageName
try {
packageName = decodeURIComponent(path.posix.basename(url.pathname))
} catch {
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_URL_INVALID')
}
if (!packageName || packageName === '.' || packageName === '..') fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_NAME_INVALID')
if (!url.pathname.startsWith(`${publicPrefix}/`) || url.pathname === `${publicPrefix}/latest.json` || url.pathname === `${publicPrefix}/health`) {
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_PUBLIC_PREFIX_MISMATCH')
}
return packageName
}
function addArtifact(artifacts, source, name, type) {
const previous = artifacts.get(name)
if (previous && previous.source !== source) fail('HOLOLAKE_RELEASE_OPERATOR_ARTIFACT_NAME_COLLISION')
artifacts.set(name, { source, name, type })
}
export function inspectReleaseBundle(
sourceDirectory,
humanApprovalPath,
{ enforceRootOwner = false, publicPrefix = DEFAULT_PUBLIC_PREFIX } = {},
) {
const source = path.resolve(sourceDirectory)
const normalizedPublicPrefix = normalizePublicPrefix(publicPrefix)
requireTrustedDirectory(source, { enforceRootOwner })
const latestPath = directFile(source, 'latest.json', 'HOLOLAKE_RELEASE_OPERATOR_BROADCAST_PATH_INVALID')
const pipelinePath = directFile(source, 'pipeline-receipt.json', 'HOLOLAKE_RELEASE_OPERATOR_PIPELINE_PATH_INVALID')
const approvalPath = path.resolve(humanApprovalPath)
const latestDocument = readJson(latestPath, { enforceRootOwner })
const pipelineReceipt = readJson(pipelinePath, { enforceRootOwner }).value
const approvalReceipt = readJson(approvalPath, { enforceRootOwner }).value
const broadcast = latestDocument.value
const broadcastSha256 = sha256(latestDocument.bytes)
if (broadcast?.schema !== 'hololake.release-broadcast/v1') fail('HOLOLAKE_RELEASE_OPERATOR_BROADCAST_INVALID')
const releaseId = requireText(broadcast.releaseId, 'HOLOLAKE_RELEASE_OPERATOR_RELEASE_ID_REQUIRED')
const version = requireText(broadcast.version, 'HOLOLAKE_RELEASE_OPERATOR_VERSION_REQUIRED')
if (!VERSION_PATTERN.test(version)) fail('HOLOLAKE_RELEASE_OPERATOR_VERSION_INVALID')
if (
broadcast?.hololake?.restart?.required !== true ||
broadcast?.hololake?.restart?.automaticAllowed !== false ||
broadcast?.hololake?.rollback?.supported !== true ||
broadcast?.hololake?.rollback?.healthReceiptRequired !== true ||
!broadcast.platforms ||
typeof broadcast.platforms !== 'object' ||
Array.isArray(broadcast.platforms) ||
Object.keys(broadcast.platforms).length === 0
) fail('HOLOLAKE_RELEASE_OPERATOR_BROADCAST_POLICY_INVALID')
if (
pipelineReceipt?.schema !== 'hololake.signed-release-pipeline-receipt/v1' ||
pipelineReceipt?.state !== 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD' ||
pipelineReceipt?.broadcastSha256 !== broadcastSha256 ||
!SOURCE_COMMIT_PATTERN.test(pipelineReceipt?.sourceCommit || '') ||
pipelineReceipt?.automaticUpload !== false ||
pipelineReceipt?.automaticActivation !== false
) fail('HOLOLAKE_RELEASE_OPERATOR_PIPELINE_RECEIPT_INVALID')
const approvalId = requireText(approvalReceipt?.approvalId, 'HOLOLAKE_RELEASE_OPERATOR_APPROVAL_ID_REQUIRED')
if (
approvalReceipt?.schema !== 'hololake.release-broadcast-human-approval/v1' ||
approvalReceipt?.state !== 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE' ||
approvalReceipt?.releaseId !== releaseId ||
approvalReceipt?.version !== version ||
approvalReceipt?.broadcastSha256 !== broadcastSha256
) fail('HOLOLAKE_RELEASE_OPERATOR_HUMAN_APPROVAL_INVALID')
const artifacts = new Map()
addArtifact(artifacts, latestPath, 'latest.json', 'control')
addArtifact(artifacts, pipelinePath, 'pipeline-receipt.json', 'control')
addArtifact(artifacts, approvalPath, 'human-approval.json', 'control')
for (const platform of Object.values(broadcast.platforms)) {
const packageName = packageNameFromUrl(platform?.url, normalizedPublicPrefix)
const packagePath = directFile(source, packageName, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_PATH_INVALID')
const packageInfo = requireTrustedFile(packagePath, { enforceRootOwner })
if (!Number.isSafeInteger(platform.size) || platform.size <= 0 || platform.size !== packageInfo.size) {
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SIZE_MISMATCH')
}
if (!SHA256_PATTERN.test(platform.sha256 || '') || sha256(fs.readFileSync(packagePath)) !== platform.sha256) {
fail('HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SHA256_MISMATCH')
}
requireText(platform.signature, 'HOLOLAKE_RELEASE_OPERATOR_PACKAGE_SIGNATURE_REQUIRED')
const codeReceiptId = requireText(platform.platformCodeSignatureReceipt, 'HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_REQUIRED')
const notarizationReceiptId = requireText(platform.notarizationReceipt, 'HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_REQUIRED')
const codeReceiptName = `${codeReceiptId}.json`
const notarizationReceiptName = `${notarizationReceiptId}.json`
const codeReceiptPath = directFile(source, codeReceiptName, 'HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_PATH_INVALID')
const notarizationReceiptPath = directFile(source, notarizationReceiptName, 'HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_PATH_INVALID')
const codeReceipt = readJson(codeReceiptPath, { enforceRootOwner }).value
const notarizationReceipt = readJson(notarizationReceiptPath, { enforceRootOwner }).value
if (
codeReceipt?.schema !== 'hololake.platform-code-signature-receipt/v1' ||
codeReceipt?.state !== 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED' ||
codeReceipt?.sourceCommit !== pipelineReceipt.sourceCommit
) fail('HOLOLAKE_RELEASE_OPERATOR_CODESIGN_RECEIPT_INVALID')
if (
notarizationReceipt?.schema !== 'hololake.apple-notarization-receipt/v1' ||
notarizationReceipt?.state !== 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED' ||
notarizationReceipt?.sourceCommit !== pipelineReceipt.sourceCommit
) fail('HOLOLAKE_RELEASE_OPERATOR_NOTARIZATION_RECEIPT_INVALID')
addArtifact(artifacts, packagePath, packageName, 'package')
addArtifact(artifacts, codeReceiptPath, codeReceiptName, 'control')
addArtifact(artifacts, notarizationReceiptPath, notarizationReceiptName, 'control')
}
return {
schema: 'hololake.release-broadcast-operator-plan/v1',
state: 'EXACT_SIGNED_NOTARIZED_HUMAN_APPROVED_BUNDLE_VERIFIED',
releaseId,
version,
sourceCommit: pipelineReceipt.sourceCommit,
broadcastSha256,
approvalId,
publicPrefix: normalizedPublicPrefix,
artifacts: [...artifacts.values()],
}
}
function copyFileNoFollow(source, destination, mode) {
const sourceDescriptor = fs.openSync(source, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW)
let destinationDescriptor = null
try {
const sourceInfo = fs.fstatSync(sourceDescriptor)
if (!sourceInfo.isFile() || (sourceInfo.mode & 0o022) !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_SOURCE_CHANGED')
destinationDescriptor = fs.openSync(destination, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, mode)
const buffer = Buffer.allocUnsafe(1024 * 1024)
let position = 0
while (position < sourceInfo.size) {
const read = fs.readSync(sourceDescriptor, buffer, 0, Math.min(buffer.length, sourceInfo.size - position), position)
if (read <= 0) fail('HOLOLAKE_RELEASE_OPERATOR_SOURCE_TRUNCATED')
let written = 0
while (written < read) written += fs.writeSync(destinationDescriptor, buffer, written, read - written)
position += read
}
fs.fsyncSync(destinationDescriptor)
} finally {
if (destinationDescriptor !== null) fs.closeSync(destinationDescriptor)
fs.closeSync(sourceDescriptor)
}
}
function writeJsonExclusive(file, value, mode) {
const descriptor = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, mode)
try {
fs.writeFileSync(descriptor, `${JSON.stringify(value, null, 2)}\n`)
fs.fsyncSync(descriptor)
} finally {
fs.closeSync(descriptor)
}
}
function activationFor(plan) {
return {
schema: 'hololake.release-broadcast-activation/v1',
state: 'HUMAN_APPROVED_SIGNED_NOTARIZED_RELEASE_ACTIVE',
releaseId: plan.releaseId,
version: plan.version,
broadcastRelativePath: `releases/${plan.version}/latest.json`,
broadcastSha256: plan.broadcastSha256,
pipelineReceiptRelativePath: `releases/${plan.version}/pipeline-receipt.json`,
humanApprovalReceipt: plan.approvalId,
humanApprovalReceiptRelativePath: `releases/${plan.version}/human-approval.json`,
}
}
function prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupId }) {
const releases = path.join(candidateRoot, 'releases')
const releaseDirectory = path.join(releases, plan.version)
fs.mkdirSync(releaseDirectory, { recursive: true, mode: enforceRootOwner ? 0o750 : 0o700 })
for (const artifact of plan.artifacts) {
const destination = path.join(releaseDirectory, artifact.name)
copyFileNoFollow(artifact.source, destination, enforceRootOwner ? 0o640 : 0o600)
if (enforceRootOwner) fs.chownSync(destination, 0, trustedGroupId)
}
const activationPath = path.join(candidateRoot, 'ACTIVE.json')
writeJsonExclusive(activationPath, activationFor(plan), enforceRootOwner ? 0o640 : 0o600)
if (enforceRootOwner) {
for (const directory of [candidateRoot, releases, releaseDirectory]) {
fs.chownSync(directory, 0, trustedGroupId)
fs.chmodSync(directory, 0o750)
}
fs.chownSync(activationPath, 0, trustedGroupId)
}
const state = loadRuntimeState(candidateRoot, { enforceRootOwner, publicPrefix: plan.publicPrefix })
if (state.state !== 'READY_SIGNED_NOTARIZED_BROADCAST') {
fail(`HOLOLAKE_RELEASE_OPERATOR_CANDIDATE_INVALID:${state.reasonCode || state.state}`)
}
return { releaseDirectory, activationPath }
}
function requireExpected(plan, expected) {
for (const [field, code] of [
['releaseId', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_RELEASE_ID_MISMATCH'],
['version', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_VERSION_MISMATCH'],
['sourceCommit', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_SOURCE_COMMIT_MISMATCH'],
['broadcastSha256', 'HOLOLAKE_RELEASE_OPERATOR_EXPECTED_BROADCAST_SHA256_MISMATCH'],
]) {
if (requireText(expected?.[field], `${code}_REQUIRED`) !== plan[field]) fail(code)
}
}
function replaceActivation(stateRoot, candidateActivation, previousBytes, enforceRootOwner, trustedGroupId, publicPrefix) {
const tempActivation = path.join(stateRoot, `.ACTIVE.${crypto.randomUUID()}.tmp`)
copyFileNoFollow(candidateActivation, tempActivation, enforceRootOwner ? 0o640 : 0o600)
if (enforceRootOwner) fs.chownSync(tempActivation, 0, trustedGroupId)
fs.renameSync(tempActivation, path.join(stateRoot, 'ACTIVE.json'))
const committed = loadRuntimeState(stateRoot, { enforceRootOwner, publicPrefix })
if (committed.state === 'READY_SIGNED_NOTARIZED_BROADCAST') return committed
const failedActive = path.join(stateRoot, 'ACTIVE.json')
if (previousBytes === null) {
fs.unlinkSync(failedActive)
} else {
const rollback = path.join(stateRoot, `.ACTIVE.rollback.${crypto.randomUUID()}.tmp`)
const descriptor = fs.openSync(rollback, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, enforceRootOwner ? 0o640 : 0o600)
try {
fs.writeFileSync(descriptor, previousBytes)
fs.fsyncSync(descriptor)
} finally {
fs.closeSync(descriptor)
}
if (enforceRootOwner) fs.chownSync(rollback, 0, trustedGroupId)
fs.renameSync(rollback, failedActive)
}
fail(`HOLOLAKE_RELEASE_OPERATOR_POST_COMMIT_INVALID:${committed.reasonCode || committed.state}`)
}
function acquireOperatorLock(stateRoot, enforceRootOwner, trustedGroupId) {
const lockDirectory = path.join(stateRoot, '.operator-lock')
try {
fs.mkdirSync(lockDirectory, { mode: enforceRootOwner ? 0o750 : 0o700 })
} catch (error) {
if (error?.code === 'EEXIST') fail('HOLOLAKE_RELEASE_OPERATOR_LOCKED')
throw error
}
try {
if (enforceRootOwner) fs.chownSync(lockDirectory, 0, trustedGroupId)
writeJsonExclusive(path.join(lockDirectory, 'owner.json'), {
schema: 'hololake.release-broadcast-operator-lock/v1',
pid: process.pid,
acquiredAt: new Date().toISOString(),
}, enforceRootOwner ? 0o640 : 0o600)
if (enforceRootOwner) fs.chownSync(path.join(lockDirectory, 'owner.json'), 0, trustedGroupId)
return lockDirectory
} catch (error) {
fs.rmSync(lockDirectory, { recursive: true, force: true })
throw error
}
}
export function verifyReleaseBundle(sourceDirectory, humanApprovalPath) {
const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath)
const candidateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-operator-verify-'))
try {
prepareCandidate(candidateRoot, plan, { enforceRootOwner: false, trustedGroupId: process.getgid?.() ?? 0 })
return { ...plan, artifacts: plan.artifacts.map(({ name, type }) => ({ name, type })) }
} finally {
fs.rmSync(candidateRoot, { recursive: true, force: true })
}
}
export function activateRelease({
sourceDirectory,
humanApprovalPath,
stateRoot,
expected,
enforceRootOwner = true,
publicPrefix = DEFAULT_PUBLIC_PREFIX,
}) {
if (enforceRootOwner && process.getuid?.() !== 0) fail('HOLOLAKE_RELEASE_OPERATOR_ROOT_REQUIRED')
const root = path.resolve(stateRoot)
const rootInfo = requireTrustedDirectory(root, { enforceRootOwner })
const trustedGroupId = rootInfo.gid
const lockDirectory = acquireOperatorLock(root, enforceRootOwner, trustedGroupId)
let candidateRoot = null
try {
const plan = inspectReleaseBundle(sourceDirectory, humanApprovalPath, { enforceRootOwner, publicPrefix })
requireExpected(plan, expected)
candidateRoot = fs.mkdtempSync(path.join(root, '.candidate-'))
const candidate = prepareCandidate(candidateRoot, plan, { enforceRootOwner, trustedGroupId })
const releasesRoot = path.join(root, 'releases')
if (!fs.existsSync(releasesRoot)) fs.mkdirSync(releasesRoot, { mode: enforceRootOwner ? 0o750 : 0o700 })
requireTrustedDirectory(releasesRoot, { enforceRootOwner })
if (enforceRootOwner) {
fs.chownSync(releasesRoot, 0, trustedGroupId)
fs.chmodSync(releasesRoot, 0o750)
}
const finalRelease = path.join(releasesRoot, plan.version)
if (fs.existsSync(finalRelease)) fail('HOLOLAKE_RELEASE_OPERATOR_VERSION_ALREADY_STAGED')
const activePath = path.join(root, 'ACTIVE.json')
const previousBytes = fs.existsSync(activePath) ? readJson(activePath, { enforceRootOwner }).bytes : null
fs.renameSync(candidate.releaseDirectory, finalRelease)
let committed
try {
committed = replaceActivation(
root,
candidate.activationPath,
previousBytes,
enforceRootOwner,
trustedGroupId,
plan.publicPrefix,
)
} catch (error) {
fs.rmSync(finalRelease, { recursive: true, force: true })
throw error
}
return {
schema: 'hololake.release-broadcast-operator-receipt/v1',
state: 'ACTIVATED_EXPLICIT_SERVICE_RESTART_REQUIRED',
releaseId: plan.releaseId,
version: plan.version,
sourceCommit: plan.sourceCommit,
broadcastSha256: plan.broadcastSha256,
approvalId: plan.approvalId,
publicPrefix: plan.publicPrefix,
automaticUpload: false,
automaticActivation: false,
automaticRestart: false,
runtimeStateAfterDiskCommit: committed.state,
}
} finally {
if (candidateRoot) fs.rmSync(candidateRoot, { recursive: true, force: true })
fs.rmSync(lockDirectory, { recursive: true, force: true })
}
}
function parseArguments(argv) {
const command = argv[0]
const values = {}
for (let index = 1; index < argv.length; index += 1) {
const key = argv[index]
if (!key.startsWith('--') || index + 1 >= argv.length) fail('HOLOLAKE_RELEASE_OPERATOR_ARGUMENT_INVALID')
values[key.slice(2)] = argv[++index]
}
return { command, values }
}
export function main(argv = process.argv.slice(2)) {
const { command, values } = parseArguments(argv)
const sourceDirectory = requireText(values.source, 'HOLOLAKE_RELEASE_OPERATOR_SOURCE_REQUIRED')
const humanApprovalPath = requireText(values['human-approval'], 'HOLOLAKE_RELEASE_OPERATOR_HUMAN_APPROVAL_PATH_REQUIRED')
if (command === 'verify') {
process.stdout.write(`${JSON.stringify(verifyReleaseBundle(sourceDirectory, humanApprovalPath), null, 2)}\n`)
return
}
if (command !== 'activate') fail('HOLOLAKE_RELEASE_OPERATOR_COMMAND_INVALID:verify|activate')
const receipt = activateRelease({
sourceDirectory,
humanApprovalPath,
stateRoot: requireText(values['state-root'], 'HOLOLAKE_RELEASE_OPERATOR_STATE_ROOT_REQUIRED'),
publicPrefix: values['public-prefix'] || DEFAULT_PUBLIC_PREFIX,
expected: {
releaseId: values['expect-release-id'],
version: values['expect-version'],
sourceCommit: values['expect-source-commit'],
broadcastSha256: values['expect-broadcast-sha256'],
},
})
process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`)
}
if (isMainModule(process.argv[1], import.meta.url)) {
try {
main()
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
process.exitCode = 1
}
}