Peer admission and bootstrap controls - Fail closed when BARE_OS_PEER_ALLOWLIST_HEX is empty unless explicit break-glass BARE_OS_PEER_ALLOW_ALL=1. - Treat BARE_OS_ZERO_TRUST_PROFILE=strict|security like strict admission posture alongside BARE_OS_PEER_ALLOWLIST_STRICT. - Document BARE_OS_PEER_ALLOW_ALL and profile semantics; update boot trust model operator guidance. Peer system seed and provenance - In strict/security profile, peer system seed defaults off unless BARE_OS_PEER_SYSTEM_SEED is explicitly enabled (1/true/yes). - Disable synthetic capability filling in strict profile; keep compat path when profile is not strict. - Extend test.peer-system-seed.js for strict default-off and no-synthesis. Path capability signer trust - When BARE_OS_PATH_CAPABILITY_ENFORCE_READ is on, require trusted issuer if BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER is set or profile is strict; wire verifyPathCapabilityEnvelopeTrusted into the primary deny path. - Document BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER and trusted key list usage in environment appendix. Host delegates (least privilege) - Under strict/security profile, empty BARE_OS_DELEGATE_ALLOW means deny-all delegates instead of allow-all; document behavior. - Add delegate strict-profile test coverage. Audit durability and telemetry hygiene - Retain audit chain rows in memory and add bareOsAuditPersistRows for optional NDJSON persistence via VFS. - Broaden var-log redaction for secret-shaped strings and env-like assignments. - Emit boot.log security line when unsafe trust combinations are detected. Release and CI gates - Add scripts/verify-zero-trust-gates.mjs and npm run verify:zero-trust-gates. - Document verifier in scripts/README.md and zero-trust steps in docs/release-checklist.md. Tests - Update bare-os-booter admission tests for allow-all and empty-allowlist messaging. - Relax brittle man.json page-count equality to a minimal sanity check to avoid brittle/os.cwd brittle failures on inventory drift. Verification (local): npm run verify:zero-trust-gates; npm run test -w bare-os-booter; peer-system-seed brittle lane as applicable. Plan file (.cursor/plans/zero-trust-boot-runtime-100-plan_*.plan.md) was not edited per instructions.
139 lines
4.0 KiB
JavaScript
139 lines
4.0 KiB
JavaScript
/**
|
|
* Tamper-evident in-session audit chain (hash-linked NDJSON rows; no persistence unless host appends).
|
|
*/
|
|
import bareCrypto from 'bare-crypto'
|
|
import b4a from 'b4a'
|
|
|
|
const { createHash } = bareCrypto
|
|
let nodeCreateHash = null
|
|
try {
|
|
const nodeCrypto = await import('crypto')
|
|
if (typeof nodeCrypto?.createHash === 'function') {
|
|
nodeCreateHash = nodeCrypto.createHash
|
|
}
|
|
} catch {
|
|
/* Bare/Pear runtime: no Node crypto module */
|
|
}
|
|
|
|
/** @type {string | null} */
|
|
let chainHeadHex = null
|
|
let seq = 0
|
|
/** @type {Array<{ seq:number, payload: Record<string, unknown>, entryHash:string, prevHash:string|null }>} */
|
|
const auditRows = []
|
|
|
|
function hexU8(u8) {
|
|
let s = ''
|
|
for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0')
|
|
return s
|
|
}
|
|
|
|
/**
|
|
* @param {string | null} prev
|
|
* @param {string} payload
|
|
* @returns {Uint8Array}
|
|
*/
|
|
function sha256LinkDigest(prev, payload) {
|
|
if (nodeCreateHash) {
|
|
const h = nodeCreateHash('sha256')
|
|
if (prev) h.update(prev, 'utf8')
|
|
h.update('\n', 'utf8')
|
|
h.update(payload, 'utf8')
|
|
const d = h.digest()
|
|
return d instanceof Uint8Array ? d : new Uint8Array(d)
|
|
}
|
|
const h = createHash('sha256')
|
|
if (prev) h.update(b4a.from(String(prev), 'utf8'))
|
|
h.update(b4a.from('\n', 'utf8'))
|
|
h.update(b4a.from(String(payload), 'utf8'))
|
|
const digest = h.digest()
|
|
return digest instanceof Uint8Array ? digest : new Uint8Array(digest)
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} entry
|
|
* @returns {{ seq: number, entryHash: string, prevHash: string | null }}
|
|
*/
|
|
export function bareOsAuditAppend(entry) {
|
|
seq++
|
|
const prev = chainHeadHex
|
|
const safeEntry = /** @type {Record<string, unknown>} */ (entry || {})
|
|
const payload = JSON.stringify(safeEntry)
|
|
const u8 = sha256LinkDigest(prev, payload)
|
|
chainHeadHex = hexU8(u8)
|
|
auditRows.push({
|
|
seq,
|
|
payload: safeEntry,
|
|
entryHash: chainHeadHex,
|
|
prevHash: prev
|
|
})
|
|
return { seq, entryHash: chainHeadHex, prevHash: prev }
|
|
}
|
|
|
|
/**
|
|
* Append a single chain link whose payload is a batch of sub-entries (one Merkle-style update).
|
|
* @param {Record<string, unknown>[]} entries
|
|
* @returns {{ seq: number, entryHash: string, prevHash: string | null } | null}
|
|
*/
|
|
export function bareOsAuditAppendBatch(entries) {
|
|
if (!Array.isArray(entries) || entries.length === 0) return null
|
|
return bareOsAuditAppend({
|
|
kind: 'batch',
|
|
batchSize: entries.length,
|
|
entries: entries.slice(0, 256)
|
|
})
|
|
}
|
|
|
|
export function bareOsAuditChainHead() {
|
|
return chainHeadHex
|
|
}
|
|
|
|
export function bareOsAuditChainLength() {
|
|
return seq
|
|
}
|
|
|
|
export function bareOsAuditChainSnapshot() {
|
|
return {
|
|
schema: 1,
|
|
length: seq,
|
|
headHex: chainHeadHex,
|
|
atMs: Date.now()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persist current audit rows as NDJSON (append-only host/logical sink path).
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} logicalPath
|
|
*/
|
|
export async function bareOsAuditPersistRows(ctx, logicalPath) {
|
|
const vfs = ctx?.vfs
|
|
if (!vfs || typeof vfs.writeFile !== 'function' || typeof ctx?.b4a?.from !== 'function') {
|
|
return false
|
|
}
|
|
const out = auditRows
|
|
.map((r) => JSON.stringify({ schema: 1, ...r }))
|
|
.join('\n')
|
|
await vfs.writeFile(String(logicalPath || '/var/log/bare-os/audit-chain.ndjson'), ctx.b4a.from(out + '\n'))
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Verify a sequence of audit rows with hash-link integrity.
|
|
* Rows must contain a stable payload object at `row.payload` and expected hash at `row.entryHash`.
|
|
* @param {{ payload: Record<string, unknown>, entryHash: string }[]} rows
|
|
* @returns {{ ok: boolean, badIndex: number, expected?: string, got?: string }}
|
|
*/
|
|
export function bareOsAuditVerifyRows(rows) {
|
|
let prev = null
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const r = rows[i]
|
|
const payload = JSON.stringify(r.payload || {})
|
|
const u8 = sha256LinkDigest(prev, payload)
|
|
const want = hexU8(u8)
|
|
const got = String(r.entryHash || '')
|
|
if (want !== got) return { ok: false, badIndex: i, expected: want, got }
|
|
prev = got
|
|
}
|
|
return { ok: true, badIndex: -1 }
|
|
}
|