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.
193 lines
6.7 KiB
JavaScript
193 lines
6.7 KiB
JavaScript
/**
|
|
* Ed25519-signed path capability envelopes (schema 1) for optional VFS read enforcement.
|
|
* Store **`user.bareos.cap_v1`** in **`PATH.bare_xattr.json`** (see `/bin/xattr`) as base64 UTF-8 JSON.
|
|
*/
|
|
import b4a from 'b4a'
|
|
import { verifyBootManifestEd25519 } from '#bare-os-boot-manifest-sig'
|
|
|
|
/**
|
|
* @param {unknown} v
|
|
* @returns {string}
|
|
*/
|
|
function stableJson(v) {
|
|
if (v === null || typeof v !== 'object') return JSON.stringify(v)
|
|
if (Array.isArray(v)) {
|
|
return '[' + v.map((x) => stableJson(x)).join(',') + ']'
|
|
}
|
|
const o = /** @type {Record<string, unknown>} */ (v)
|
|
const keys = Object.keys(o).sort()
|
|
return (
|
|
'{' +
|
|
keys.map((k) => JSON.stringify(k) + ':' + stableJson(o[k])).join(',') +
|
|
'}'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} payload
|
|
*/
|
|
export function bareOsPathCapabilityPayloadCanonicalUtf8(payload) {
|
|
return b4a.from(stableJson(payload), 'utf8')
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} envelope
|
|
* @returns {{ ok: true, payload: { prefix: string, ops: string[], expMs: number | null } } | { ok: false, reason: string }}
|
|
*/
|
|
export function verifyPathCapabilityEnvelope(envelope) {
|
|
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
|
return { ok: false, reason: 'envelope_not_object' }
|
|
}
|
|
const e = /** @type {Record<string, unknown>} */ (envelope)
|
|
if (Number(e.schema) !== 1) return { ok: false, reason: 'bad_schema' }
|
|
const payload = e.payload
|
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
return { ok: false, reason: 'bad_payload' }
|
|
}
|
|
const pl = /** @type {Record<string, unknown>} */ (payload)
|
|
const prefix = String(pl.prefix || '').trim()
|
|
if (!prefix.startsWith('/')) return { ok: false, reason: 'bad_prefix' }
|
|
const ops = Array.isArray(pl.ops) ? pl.ops.map((x) => String(x)) : []
|
|
if (!ops.includes('read') && !ops.includes('all')) {
|
|
return { ok: false, reason: 'no_read_op' }
|
|
}
|
|
const expRaw = pl.expMs
|
|
const expMs =
|
|
expRaw != null && Number.isFinite(Number(expRaw)) ? Number(expRaw) : null
|
|
if (expMs != null && Date.now() > expMs) return { ok: false, reason: 'expired' }
|
|
|
|
const msg = bareOsPathCapabilityPayloadCanonicalUtf8(pl)
|
|
const sig = String(e.signatureHex || '').trim().replace(/^0x/, '')
|
|
const pub = String(e.pubkeyHex || '').trim().replace(/^0x/, '')
|
|
if (!/^[0-9a-f]{128}$/i.test(sig)) return { ok: false, reason: 'bad_signature_len' }
|
|
if (!/^[0-9a-f]{64}$/i.test(pub)) return { ok: false, reason: 'bad_pubkey_len' }
|
|
const ok = verifyBootManifestEd25519(msg, sig, pub)
|
|
if (!ok) return { ok: false, reason: 'bad_signature' }
|
|
return { ok: true, payload: { prefix, ops, expMs } }
|
|
}
|
|
|
|
/**
|
|
* @param {string} b64
|
|
*/
|
|
function xattrB64ToUtf8(b64) {
|
|
const t = String(b64 || '').replace(/\s+/g, '')
|
|
try {
|
|
return b4a.toString(b4a.from(t, 'base64'), 'utf8')
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When **`BARE_OS_PATH_CAPABILITY_ENFORCE_READ`** is set, personal-drive reads under
|
|
* **`BARE_OS_PATH_CAPABILITY_PREFIX`** require a valid **`user.bareos.cap_v1`** xattr envelope
|
|
* on **`PATH.bare_xattr.json`** authorizing the logical path prefix.
|
|
*
|
|
* @param {Record<string, string>} env
|
|
* @param {import('hyperdrive').default} drive
|
|
* @param {string} drivePath path segment on drive (as `drive.get` expects)
|
|
* @param {string} logicalAbs resolved logical absolute path (e.g. `/home/u/f`)
|
|
*/
|
|
export async function bareOsVfsPathCapabilityDeniesDriveRead(
|
|
env,
|
|
drive,
|
|
drivePath,
|
|
logicalAbs
|
|
) {
|
|
const on =
|
|
env.BARE_OS_PATH_CAPABILITY_ENFORCE_READ === '1' ||
|
|
env.BARE_OS_PATH_CAPABILITY_ENFORCE_READ === 'true'
|
|
if (!on || !drive || typeof drive.get !== 'function') return false
|
|
const root = String(env.BARE_OS_PATH_CAPABILITY_PREFIX || '/home/').trim()
|
|
if (!root.startsWith('/')) return false
|
|
const la = String(logicalAbs || '').replace(/\/+$/, '') || '/'
|
|
if (!la.startsWith(root)) return false
|
|
if (la.endsWith('.bare_xattr.json') || la.endsWith('.bare_acl')) return false
|
|
|
|
const p = String(drivePath || '').replace(/\/+$/, '') || '/'
|
|
const sidePath = p + '.bare_xattr.json'
|
|
let raw
|
|
try {
|
|
raw = await drive.get(sidePath, { follow: false })
|
|
} catch {
|
|
return true
|
|
}
|
|
if (!raw || !raw.byteLength) return true
|
|
let map
|
|
try {
|
|
map = JSON.parse(b4a.toString(raw))
|
|
} catch {
|
|
return true
|
|
}
|
|
if (!map || typeof map !== 'object') return true
|
|
const b64 = map['user.bareos.cap_v1']
|
|
if (typeof b64 !== 'string' || !b64.trim()) return true
|
|
let inner
|
|
try {
|
|
inner = JSON.parse(xattrB64ToUtf8(b64))
|
|
} catch {
|
|
return true
|
|
}
|
|
const vr = verifyPathCapabilityEnvelope(inner)
|
|
if (!vr.ok) return true
|
|
const strictTrusted =
|
|
env.BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER === '1' ||
|
|
env.BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER === 'true' ||
|
|
String(env.BARE_OS_ZERO_TRUST_PROFILE || '')
|
|
.trim()
|
|
.toLowerCase() === 'strict'
|
|
if (strictTrusted) {
|
|
const tr = verifyPathCapabilityEnvelopeTrusted(inner, env)
|
|
if (!tr.ok || tr.trustedIssuer !== true) return true
|
|
}
|
|
if (!la.startsWith(vr.payload.prefix)) return true
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, string | undefined> | null | undefined} env
|
|
* @returns {string[] | null}
|
|
*/
|
|
export function parsePathCapabilityTrustedPubkeysHex(env) {
|
|
const raw = String(env?.BARE_OS_PATH_CAPABILITY_TRUSTED_PUBKEYS_HEX || '').trim()
|
|
if (!raw) return null
|
|
/** @type {string[]} */
|
|
const out = []
|
|
for (const p of raw.split(/[,;\s]+/)) {
|
|
const x = p.trim().toLowerCase().replace(/^0x/, '')
|
|
if (/^[0-9a-f]{64}$/i.test(x)) out.push(x.toLowerCase())
|
|
}
|
|
return out.length ? out : null
|
|
}
|
|
|
|
/**
|
|
* Same as {@link verifyPathCapabilityEnvelope} plus optional issuer allowlist from
|
|
* **`BARE_OS_PATH_CAPABILITY_TRUSTED_PUBKEYS_HEX`** (comma-separated 64-hex Ed25519 pubkeys).
|
|
* @param {unknown} envelope
|
|
* @param {Record<string, string | undefined> | null | undefined} env
|
|
* @returns {{ ok: true, payload: { prefix: string, ops: string[], expMs: number | null }, trustedIssuer: true | 'signature_only' } | { ok: false, reason: string }}
|
|
*/
|
|
export function verifyPathCapabilityEnvelopeTrusted(envelope, env) {
|
|
const base = verifyPathCapabilityEnvelope(envelope)
|
|
if (!base.ok) return base
|
|
const allow = parsePathCapabilityTrustedPubkeysHex(env)
|
|
if (!allow) {
|
|
return {
|
|
ok: true,
|
|
payload: base.payload,
|
|
trustedIssuer: 'signature_only'
|
|
}
|
|
}
|
|
const e =
|
|
envelope && typeof envelope === 'object' && !Array.isArray(envelope)
|
|
? /** @type {Record<string, unknown>} */ (envelope)
|
|
: {}
|
|
const pub = String(e.pubkeyHex || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/^0x/, '')
|
|
if (!allow.includes(pub))
|
|
return { ok: false, reason: 'issuer_not_trusted' }
|
|
return { ok: true, payload: base.payload, trustedIssuer: true }
|
|
}
|