diff --git a/product-source/hololake-native-desktop/scripts/release-broadcast-server.test.mjs b/product-source/hololake-native-desktop/scripts/release-broadcast-server.test.mjs index e7cd050..565fc68 100644 --- a/product-source/hololake-native-desktop/scripts/release-broadcast-server.test.mjs +++ b/product-source/hololake-native-desktop/scripts/release-broadcast-server.test.mjs @@ -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) +}) diff --git a/product-source/hololake-native-desktop/server/release-broadcast/server.mjs b/product-source/hololake-native-desktop/server/release-broadcast/server.mjs index 818fca3..39024fe 100644 --- a/product-source/hololake-native-desktop/server/release-broadcast/server.mjs +++ b/product-source/hololake-native-desktop/server/release-broadcast/server.mjs @@ -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) {