- HRPC: bare_os.pkg_index_get, route table schema 3; pkg-swarm-index list/get; pathcap-verify --trusted - POSIX: profile 1.0.17, ctx API 1.53.0, syscalls.json schema 11 + susv4Refs; JSON schemas + matrix/dashboard - Feature bits: BARE_OS_KERNEL_FEATURE_BITS_DOC 16; contract + verify scripts; ctx.d.ts + gen helper sync - Ops: BARE_OS_HOLEPUNCH_DRIFT_TIER1 + tier1Repos; mktemp avoids false XXX marker; /proc boot_budget_summary test list - Docs: contract spine, env appendix, handbook, compatibility matrix, boot budget schema, vault threat model notes Covers bare-os P2P roadmap items 1–20 where implemented in-tree; kernel/lib/bare/README left minimal per maintainer edit.
183 lines
6.3 KiB
JavaScript
183 lines
6.3 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
|
|
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 }
|
|
}
|