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.
236 lines
6.7 KiB
JavaScript
236 lines
6.7 KiB
JavaScript
/**
|
||
* Optional peer admission gate from **`BARE_OS_PEER_ALLOWLIST_HEX`**
|
||
* (comma-separated hex public keys; empty = deny unless explicit allow-all).
|
||
*
|
||
* **`BARE_OS_PEER_DENYLIST_HEX`** — comma-separated hex keys; if the peer matches,
|
||
* verdict is **`deny`** before allowlist evaluation (denylist wins).
|
||
*
|
||
* **`BARE_OS_PEER_REQUIRE_CAPS_JSON`** — JSON array of capability strings; when set,
|
||
* **`meta.caps`** must include every required token (hosts pass peer caps when known).
|
||
*
|
||
* When **`BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST`** is set and callers pass
|
||
* **`dhtAddressClass`** (hyperdht-address–style hint), the class must appear in
|
||
* the allow list after the key gate passes.
|
||
*
|
||
* Admission results use **`schema: 2`** (v1 was **`schema: 1`** only).
|
||
*
|
||
* @param {Record<string, unknown> | null | undefined} env
|
||
* @param {string} peerKeyHex
|
||
* @param {{ dhtAddressClass?: string, caps?: string[] } | null | undefined} [meta]
|
||
*/
|
||
const ADMISSION_SCHEMA = 2
|
||
|
||
/**
|
||
* @param {Record<string, unknown> | null | undefined} env
|
||
*/
|
||
function zeroTrustStrictProfile(env) {
|
||
const p = String(env?.BARE_OS_ZERO_TRUST_PROFILE || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
return p === 'strict' || p === 'security'
|
||
}
|
||
|
||
/** @param {string} s */
|
||
function normKeyHex(s) {
|
||
return String(s || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/^0x/, '')
|
||
}
|
||
|
||
/** @param {string} raw */
|
||
function keySetFromCommaHex(raw) {
|
||
return new Set(
|
||
raw
|
||
.split(/[\s,]+/)
|
||
.map((s) => normKeyHex(s))
|
||
.filter(Boolean)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown> | null | undefined} env
|
||
* @returns {string[] | null}
|
||
*/
|
||
function parseRequireCaps(env) {
|
||
const raw = String(env?.BARE_OS_PEER_REQUIRE_CAPS_JSON || '').trim()
|
||
if (!raw) return null
|
||
try {
|
||
const j = JSON.parse(raw)
|
||
if (!Array.isArray(j)) return null
|
||
const caps = j
|
||
.map((x) => String(x).trim())
|
||
.filter(Boolean)
|
||
.slice(0, 32)
|
||
return caps.length ? caps : null
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
export function evaluateBareOsPeerAdmission(env, peerKeyHex, meta = undefined) {
|
||
const want = normKeyHex(peerKeyHex)
|
||
const atMs = () => Date.now()
|
||
|
||
const denyRaw = String(env?.BARE_OS_PEER_DENYLIST_HEX || '').trim()
|
||
if (denyRaw) {
|
||
const denySet = keySetFromCommaHex(denyRaw)
|
||
if (want && denySet.has(want)) {
|
||
return {
|
||
schema: ADMISSION_SCHEMA,
|
||
verdict: 'deny',
|
||
reason: 'peer_denylist',
|
||
denylistSize: denySet.size,
|
||
atMs: atMs()
|
||
}
|
||
}
|
||
}
|
||
|
||
const requireCaps = parseRequireCaps(env)
|
||
if (requireCaps && requireCaps.length > 0) {
|
||
const peerCapsRaw = meta && Array.isArray(meta.caps) ? meta.caps : []
|
||
const peerSet = new Set(
|
||
peerCapsRaw.map((c) => String(c).trim()).filter(Boolean)
|
||
)
|
||
for (const c of requireCaps) {
|
||
if (!peerSet.has(c)) {
|
||
return {
|
||
schema: ADMISSION_SCHEMA,
|
||
verdict: 'deny',
|
||
reason: 'peer_missing_cap',
|
||
requiredCaps: [...requireCaps],
|
||
atMs: atMs()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
const raw = String(env?.BARE_OS_PEER_ALLOWLIST_HEX || '').trim()
|
||
const strict =
|
||
env?.BARE_OS_PEER_ALLOWLIST_STRICT === '1' ||
|
||
env?.BARE_OS_PEER_ALLOWLIST_STRICT === 'true' ||
|
||
zeroTrustStrictProfile(env)
|
||
const allowAll =
|
||
env?.BARE_OS_PEER_ALLOW_ALL === '1' || env?.BARE_OS_PEER_ALLOW_ALL === 'true'
|
||
/** @type {{ schema: number, verdict: string, note?: string, allowlistSize?: number, strictProfile?: boolean, atMs?: number, dhtAddressClassGate?: Record<string, unknown> }} */
|
||
let base
|
||
if (!raw) {
|
||
if (strict || !allowAll) {
|
||
base = {
|
||
schema: ADMISSION_SCHEMA,
|
||
verdict: 'deny',
|
||
reason: 'peer_allowlist_empty',
|
||
note:
|
||
'Peer allowlist is empty; admission denied unless BARE_OS_PEER_ALLOW_ALL=1.'
|
||
}
|
||
} else {
|
||
base = {
|
||
schema: ADMISSION_SCHEMA,
|
||
verdict: 'allow',
|
||
note: 'No allowlist and BARE_OS_PEER_ALLOW_ALL=1; peers admitted.'
|
||
}
|
||
}
|
||
} else {
|
||
const set = keySetFromCommaHex(raw)
|
||
const ok = want && set.has(want)
|
||
base = {
|
||
schema: ADMISSION_SCHEMA,
|
||
verdict: ok ? 'allow' : 'deny',
|
||
allowlistSize: set.size,
|
||
strictProfile: strict || undefined,
|
||
atMs: atMs()
|
||
}
|
||
}
|
||
|
||
const classRaw = String(env?.BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST || '').trim()
|
||
if (!classRaw) {
|
||
if (base.atMs == null) base.atMs = atMs()
|
||
return base
|
||
}
|
||
|
||
const allowClasses = new Set(
|
||
classRaw.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)
|
||
)
|
||
if (allowClasses.size === 0) {
|
||
if (base.atMs == null) base.atMs = atMs()
|
||
return base
|
||
}
|
||
|
||
const cls = String(meta?.dhtAddressClass || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
if (!cls) {
|
||
return {
|
||
...base,
|
||
atMs: base.atMs ?? atMs(),
|
||
dhtAddressClassGate: {
|
||
schema: 1,
|
||
mode: 'no_peer_class_hint',
|
||
note: 'BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST is set but caller did not supply dhtAddressClass — stock Hyperswarm path does not classify peers; allow key verdict only.'
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!allowClasses.has(cls)) {
|
||
return {
|
||
schema: ADMISSION_SCHEMA,
|
||
verdict: 'deny',
|
||
reason: 'dht_address_class',
|
||
dhtAddressClass: cls,
|
||
allowedClasses: [...allowClasses],
|
||
note: 'Peer DHT address class not listed in BARE_OS_DHT_ADDRESS_CLASS_ALLOWLIST.',
|
||
atMs: atMs()
|
||
}
|
||
}
|
||
|
||
if (base.verdict === 'deny') return base
|
||
|
||
return {
|
||
...base,
|
||
atMs: base.atMs ?? atMs(),
|
||
dhtAddressClassGate: { schema: 1, mode: 'allow', class: cls }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Build an event-bus row for **`peer_admission`** audit (**never** includes full peer keys).
|
||
*
|
||
* @param {string} peerKeyHex
|
||
* @param {{ verdict: string, reason?: string, schema: number }} result
|
||
* @param {{ dhtAddressClass?: string } | null | undefined} [meta]
|
||
*/
|
||
export function bareOsFormatPeerAdmissionAuditEvent(peerKeyHex, result, meta) {
|
||
const norm = String(peerKeyHex || '')
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/^0x/, '')
|
||
const keyShort = norm ? norm.slice(0, 16) : ''
|
||
return {
|
||
type: 'peer_admission',
|
||
verdict: result.verdict,
|
||
reason: result.reason,
|
||
peerKeyHexPrefix: keyShort || undefined,
|
||
admissionSchema: result.schema,
|
||
dhtAddressClass:
|
||
meta && typeof meta === 'object'
|
||
? String(meta.dhtAddressClass || '').trim() || undefined
|
||
: undefined
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Per-bucket rate gate for admission audit (**mutates** **`lastMap`** on allow).
|
||
*
|
||
* @param {string} bucket
|
||
* @param {number} now
|
||
* @param {number} rateMs
|
||
* @param {Map<string, number>} lastMap
|
||
*/
|
||
export function bareOsPeerAdmissionAuditRateAllow(bucket, now, rateMs, lastMap) {
|
||
if (rateMs <= 0) return true
|
||
const last = lastMap.get(bucket) ?? 0
|
||
if (now - last < rateMs) return false
|
||
lastMap.set(bucket, now)
|
||
return true
|
||
}
|