feat(hololake): add fail-closed JD release broadcast origin
This commit is contained in:
parent
df563a9fdd
commit
f73e11b364
6 changed files with 423 additions and 2 deletions
|
|
@ -45,10 +45,12 @@ test('release activation remains explicitly human controlled', () => {
|
|||
assert.equal(foundation.release_package_signature_size_sha256_verification_implemented, true)
|
||||
assert.equal(foundation.release_persistent_rollback_executor_implemented, true)
|
||||
assert.equal(foundation.release_signed_notarized_pipeline_implemented, true)
|
||||
assert.equal(foundation.release_broadcast_candidate_service_implemented, true)
|
||||
assert.equal(foundation.release_broadcast_candidate_service_default_state, 'EMPTY_FAIL_CLOSED_LOOPBACK_ONLY')
|
||||
assert.equal(foundation.release_pipeline_automatic_upload_allowed, false)
|
||||
assert.equal(
|
||||
foundation.release_production_activation_state,
|
||||
'BLOCKED_PENDING_JD_TRUST_SIGNED_PIPELINE_AND_APPLE_NOTARIZATION',
|
||||
'BLOCKED_PENDING_PUBLIC_HTTPS_TRUST_UPDATER_KEY_PIPELINE_EXECUTION_AND_APPLE_NOTARIZATION',
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
import assert from 'node:assert/strict'
|
||||
import crypto from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
|
||||
import { createReleaseServer, loadRuntimeState } from '../server/release-broadcast/server.mjs'
|
||||
|
||||
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex')
|
||||
|
||||
const writeJson = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
|
||||
|
||||
async function withServer(state, callback) {
|
||||
const server = createReleaseServer(state)
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||
const address = server.address()
|
||||
try {
|
||||
return await callback(`http://127.0.0.1:${address.port}`)
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()))
|
||||
}
|
||||
}
|
||||
|
||||
function buildReleaseRoot() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-broadcast-'))
|
||||
const releaseDirectory = path.join(root, 'releases', '0.2.0')
|
||||
fs.mkdirSync(releaseDirectory, { recursive: true })
|
||||
const packageBytes = Buffer.from('signed-notarized-updater-placeholder')
|
||||
const packageName = 'HoloLake.app.tar.gz'
|
||||
fs.writeFileSync(path.join(releaseDirectory, packageName), packageBytes, { mode: 0o600 })
|
||||
writeJson(path.join(releaseDirectory, 'HOLOLAKE-CODESIGN.json'), {
|
||||
schema: 'hololake.platform-code-signature-receipt/v1',
|
||||
state: 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED',
|
||||
})
|
||||
writeJson(path.join(releaseDirectory, 'HOLOLAKE-NOTARIZATION.json'), {
|
||||
schema: 'hololake.apple-notarization-receipt/v1',
|
||||
state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED',
|
||||
})
|
||||
const broadcast = {
|
||||
schema: 'hololake.release-broadcast/v1',
|
||||
releaseId: 'GH-HOLOLAKE-RELEASE-0.2.0',
|
||||
version: '0.2.0',
|
||||
pub_date: '2026-08-13T10:00:00.000Z',
|
||||
notes: 'Signed release',
|
||||
platforms: {
|
||||
'darwin-aarch64': {
|
||||
url: `https://release.guanghu.test/releases/0.2.0/${packageName}`,
|
||||
signature: 'trusted-updater-signature',
|
||||
size: packageBytes.length,
|
||||
sha256: sha256(packageBytes),
|
||||
platformCodeSignatureReceipt: 'HOLOLAKE-CODESIGN',
|
||||
notarizationReceipt: 'HOLOLAKE-NOTARIZATION',
|
||||
},
|
||||
},
|
||||
hololake: {
|
||||
features: ['Stage one'],
|
||||
fixes: [],
|
||||
compatibility: { minimumVersion: '0.1.0', dataMigrationRequired: false },
|
||||
restart: { required: true, automaticAllowed: false },
|
||||
rollback: { supported: true, healthReceiptRequired: true, previousVersion: '0.1.0' },
|
||||
},
|
||||
}
|
||||
const broadcastPath = path.join(releaseDirectory, 'latest.json')
|
||||
writeJson(broadcastPath, broadcast)
|
||||
const broadcastSha256 = sha256(fs.readFileSync(broadcastPath))
|
||||
writeJson(path.join(releaseDirectory, 'pipeline-receipt.json'), {
|
||||
schema: 'hololake.signed-release-pipeline-receipt/v1',
|
||||
state: 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD',
|
||||
broadcastSha256,
|
||||
automaticUpload: false,
|
||||
automaticActivation: false,
|
||||
})
|
||||
writeJson(path.join(root, 'ACTIVE.json'), {
|
||||
schema: 'hololake.release-broadcast-activation/v1',
|
||||
state: 'HUMAN_APPROVED_SIGNED_NOTARIZED_RELEASE_ACTIVE',
|
||||
releaseId: broadcast.releaseId,
|
||||
version: broadcast.version,
|
||||
broadcastRelativePath: 'releases/0.2.0/latest.json',
|
||||
broadcastSha256,
|
||||
pipelineReceiptRelativePath: 'releases/0.2.0/pipeline-receipt.json',
|
||||
humanApprovalReceipt: 'GH-HUMAN-RELEASE-APPROVAL-001',
|
||||
})
|
||||
return { root, packageBytes }
|
||||
}
|
||||
|
||||
test('empty release root stays healthy but returns Tauri-compatible 204 no update', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-empty-'))
|
||||
const state = loadRuntimeState(root)
|
||||
assert.equal(state.state, 'EMPTY_FAIL_CLOSED')
|
||||
await withServer(state, async (base) => {
|
||||
const health = await fetch(`${base}/health`)
|
||||
assert.equal(health.status, 200)
|
||||
assert.deepEqual((await health.json()).upstreamUpdateSources, [])
|
||||
assert.equal((await fetch(`${base}/latest.json`)).status, 204)
|
||||
})
|
||||
})
|
||||
|
||||
test('invalid activation locks the endpoint instead of falling back to an update', async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hololake-release-invalid-'))
|
||||
writeJson(path.join(root, 'ACTIVE.json'), { schema: 'wrong', state: 'ACTIVE' })
|
||||
const state = loadRuntimeState(root)
|
||||
assert.equal(state.state, 'LOCKED_INVALID_RELEASE_EVIDENCE')
|
||||
await withServer(state, async (base) => {
|
||||
assert.equal((await fetch(`${base}/health`)).status, 503)
|
||||
assert.equal((await fetch(`${base}/latest.json`)).status, 503)
|
||||
})
|
||||
})
|
||||
|
||||
test('only an exact human-approved signed notarized evidence chain becomes readable', async () => {
|
||||
const fixture = buildReleaseRoot()
|
||||
const state = loadRuntimeState(fixture.root)
|
||||
assert.equal(state.state, 'READY_SIGNED_NOTARIZED_BROADCAST')
|
||||
await withServer(state, async (base) => {
|
||||
const latest = await fetch(`${base}/latest.json`)
|
||||
assert.equal(latest.status, 200)
|
||||
assert.equal((await latest.json()).releaseId, 'GH-HOLOLAKE-RELEASE-0.2.0')
|
||||
const updater = await fetch(`${base}/releases/0.2.0/HoloLake.app.tar.gz`)
|
||||
assert.equal(updater.status, 200)
|
||||
assert.deepEqual(Buffer.from(await updater.arrayBuffer()), fixture.packageBytes)
|
||||
assert.equal((await fetch(`${base}/unknown`)).status, 404)
|
||||
})
|
||||
})
|
||||
|
||||
test('package tampering after pipeline output locks the whole release at startup', () => {
|
||||
const fixture = buildReleaseRoot()
|
||||
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')
|
||||
})
|
||||
Loading…
Reference in a new issue