fix(hololake): harden immutable broadcast activation
This commit is contained in:
parent
f73e11b364
commit
aa3df05775
2 changed files with 47 additions and 6 deletions
|
|
@ -5,7 +5,7 @@ import os from 'node:os'
|
|||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
import { createReleaseServer, loadRuntimeState } from '../server/release-broadcast/server.mjs'
|
||||
import { createReleaseServer, isMainModule, loadRuntimeState } from '../server/release-broadcast/server.mjs'
|
||||
|
||||
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
|
||||
|
|
@ -32,10 +32,12 @@ function buildReleaseRoot() {
|
|||
writeJson(path.join(releaseDirectory, 'HOLOLAKE-CODESIGN.json'), {
|
||||
schema: 'hololake.platform-code-signature-receipt/v1',
|
||||
state: 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED',
|
||||
sourceCommit: '1'.repeat(40),
|
||||
})
|
||||
writeJson(path.join(releaseDirectory, 'HOLOLAKE-NOTARIZATION.json'), {
|
||||
schema: 'hololake.apple-notarization-receipt/v1',
|
||||
state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED',
|
||||
sourceCommit: '1'.repeat(40),
|
||||
})
|
||||
const broadcast = {
|
||||
schema: 'hololake.release-broadcast/v1',
|
||||
|
|
@ -67,6 +69,7 @@ function buildReleaseRoot() {
|
|||
writeJson(path.join(releaseDirectory, 'pipeline-receipt.json'), {
|
||||
schema: 'hololake.signed-release-pipeline-receipt/v1',
|
||||
state: 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD',
|
||||
sourceCommit: '1'.repeat(40),
|
||||
broadcastSha256,
|
||||
automaticUpload: false,
|
||||
automaticActivation: false,
|
||||
|
|
@ -80,6 +83,15 @@ function buildReleaseRoot() {
|
|||
broadcastSha256,
|
||||
pipelineReceiptRelativePath: 'releases/0.2.0/pipeline-receipt.json',
|
||||
humanApprovalReceipt: 'GH-HUMAN-RELEASE-APPROVAL-001',
|
||||
humanApprovalReceiptRelativePath: 'releases/0.2.0/human-approval.json',
|
||||
})
|
||||
writeJson(path.join(releaseDirectory, 'human-approval.json'), {
|
||||
schema: 'hololake.release-broadcast-human-approval/v1',
|
||||
state: 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE',
|
||||
approvalId: 'GH-HUMAN-RELEASE-APPROVAL-001',
|
||||
releaseId: broadcast.releaseId,
|
||||
version: broadcast.version,
|
||||
broadcastSha256,
|
||||
})
|
||||
return { root, packageBytes }
|
||||
}
|
||||
|
|
@ -127,3 +139,15 @@ test('package tampering after pipeline output locks the whole release at startup
|
|||
fs.appendFileSync(path.join(fixture.root, 'releases', '0.2.0', 'HoloLake.app.tar.gz'), 'tampered')
|
||||
assert.equal(loadRuntimeState(fixture.root).state, 'LOCKED_INVALID_RELEASE_EVIDENCE')
|
||||
})
|
||||
|
||||
test('a symlinked immutable current directory is recognized as the intended executable', () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-symlink-'))
|
||||
const versionDirectory = path.join(root, 'immutable')
|
||||
fs.mkdirSync(versionDirectory)
|
||||
const source = fs.readFileSync(new URL('../server/release-broadcast/server.mjs', import.meta.url), 'utf8')
|
||||
const copy = path.join(versionDirectory, 'server.mjs')
|
||||
fs.writeFileSync(copy, source, { mode: 0o500 })
|
||||
const current = path.join(root, 'current')
|
||||
fs.symlinkSync(versionDirectory, current)
|
||||
assert.equal(isMainModule(path.join(current, 'server.mjs'), new URL(`file://${copy}`)), true)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function readTrustedJson(file, options) {
|
|||
return { bytes: fs.readFileSync(file), value: JSON.parse(fs.readFileSync(file, 'utf8')) }
|
||||
}
|
||||
|
||||
function validatePlatform(platform, releaseDirectory, enforceRootOwner) {
|
||||
function validatePlatform(platform, releaseDirectory, enforceRootOwner, sourceCommit) {
|
||||
const url = new URL(requireText(platform?.url, 'HOLOLAKE_RELEASE_PACKAGE_URL_REQUIRED'))
|
||||
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) {
|
||||
fail('HOLOLAKE_RELEASE_PACKAGE_URL_INVALID')
|
||||
|
|
@ -71,11 +71,13 @@ function validatePlatform(platform, releaseDirectory, enforceRootOwner) {
|
|||
).value
|
||||
if (
|
||||
codeReceipt?.schema !== 'hololake.platform-code-signature-receipt/v1' ||
|
||||
codeReceipt?.state !== 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED'
|
||||
codeReceipt?.state !== 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED' ||
|
||||
codeReceipt?.sourceCommit !== sourceCommit
|
||||
) fail('HOLOLAKE_RELEASE_CODESIGN_RECEIPT_INVALID')
|
||||
if (
|
||||
notarizationReceipt?.schema !== 'hololake.apple-notarization-receipt/v1' ||
|
||||
notarizationReceipt?.state !== 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED'
|
||||
notarizationReceipt?.state !== 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED' ||
|
||||
notarizationReceipt?.sourceCommit !== sourceCommit
|
||||
) fail('HOLOLAKE_RELEASE_NOTARIZATION_RECEIPT_INVALID')
|
||||
return { pathname: url.pathname, file: packagePath, size: packageInfo.size }
|
||||
}
|
||||
|
|
@ -97,16 +99,27 @@ function loadActiveRelease(stateRoot, enforceRootOwner) {
|
|||
requireText(activation.humanApprovalReceipt, 'HOLOLAKE_RELEASE_HUMAN_APPROVAL_REQUIRED')
|
||||
const broadcastPath = safeRelative(stateRoot, activation.broadcastRelativePath, 'HOLOLAKE_RELEASE_BROADCAST_PATH_INVALID')
|
||||
const pipelinePath = safeRelative(stateRoot, activation.pipelineReceiptRelativePath, 'HOLOLAKE_RELEASE_PIPELINE_PATH_INVALID')
|
||||
const approvalPath = safeRelative(stateRoot, activation.humanApprovalReceiptRelativePath, 'HOLOLAKE_RELEASE_HUMAN_APPROVAL_PATH_INVALID')
|
||||
const broadcastDocument = readTrustedJson(broadcastPath, { enforceRootOwner })
|
||||
const pipelineReceipt = readTrustedJson(pipelinePath, { enforceRootOwner }).value
|
||||
const approvalReceipt = readTrustedJson(approvalPath, { enforceRootOwner }).value
|
||||
if (sha256(broadcastDocument.bytes) !== activation.broadcastSha256) fail('HOLOLAKE_RELEASE_BROADCAST_SHA256_MISMATCH')
|
||||
if (
|
||||
pipelineReceipt?.schema !== 'hololake.signed-release-pipeline-receipt/v1' ||
|
||||
pipelineReceipt?.state !== 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD' ||
|
||||
pipelineReceipt?.broadcastSha256 !== activation.broadcastSha256 ||
|
||||
!/^[a-f0-9]{40}$/.test(pipelineReceipt?.sourceCommit || '') ||
|
||||
pipelineReceipt?.automaticUpload !== false ||
|
||||
pipelineReceipt?.automaticActivation !== false
|
||||
) fail('HOLOLAKE_RELEASE_PIPELINE_RECEIPT_INVALID')
|
||||
if (
|
||||
approvalReceipt?.schema !== 'hololake.release-broadcast-human-approval/v1' ||
|
||||
approvalReceipt?.state !== 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE' ||
|
||||
approvalReceipt?.approvalId !== activation.humanApprovalReceipt ||
|
||||
approvalReceipt?.releaseId !== releaseId ||
|
||||
approvalReceipt?.version !== version ||
|
||||
approvalReceipt?.broadcastSha256 !== activation.broadcastSha256
|
||||
) fail('HOLOLAKE_RELEASE_HUMAN_APPROVAL_INVALID')
|
||||
const broadcast = broadcastDocument.value
|
||||
if (
|
||||
broadcast?.schema !== 'hololake.release-broadcast/v1' ||
|
||||
|
|
@ -122,7 +135,7 @@ function loadActiveRelease(stateRoot, enforceRootOwner) {
|
|||
const releaseDirectory = path.dirname(broadcastPath)
|
||||
const packages = new Map()
|
||||
for (const platform of Object.values(broadcast.platforms)) {
|
||||
const accepted = validatePlatform(platform, releaseDirectory, enforceRootOwner)
|
||||
const accepted = validatePlatform(platform, releaseDirectory, enforceRootOwner, pipelineReceipt.sourceCommit)
|
||||
if (packages.has(accepted.pathname)) fail('HOLOLAKE_RELEASE_PACKAGE_ROUTE_COLLISION')
|
||||
packages.set(accepted.pathname, accepted)
|
||||
}
|
||||
|
|
@ -226,7 +239,11 @@ export function main(env = process.env) {
|
|||
return server
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
export function isMainModule(argvPath, moduleUrl) {
|
||||
return Boolean(argvPath) && fs.realpathSync(path.resolve(argvPath)) === fs.realpathSync(fileURLToPath(moduleUrl))
|
||||
}
|
||||
|
||||
if (isMainModule(process.argv[1], import.meta.url)) {
|
||||
try {
|
||||
main()
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue