Two-tier trust model (ADR-002) is absent from the credential layer — one trusted key grants unsandboxed native execution
Источник:
dev/fera-research/docs/oc5/trust-tier-flat-keyring-authority-collapse.md(read-only; сгенерировано scripts/sync-docs.mjs)
Two-tier trust model (ADR-002) is absent from the credential layer — one trusted key grants unsandboxed native execution
Section titled “Two-tier trust model (ADR-002) is absent from the credential layer — one trusted key grants unsandboxed native execution”- kind: risk
- area: trust / modules / security
- severity: high
- evidence:
~/dev/fera/project-design/context/ADR.md:39-62(ADR-002) — two tiers with opposite blast radii::48-51“First-party / performance-critical modules … are native dylibs … No sandbox — these are trusted-by-design, hash mismatches produce a warning, not a load refusal”:52-56“Third-party community plugins are WASM modules … sandboxed by construction. Capability-gated: read-only by default”:57-61Consequences: “Two separate loading/validation pipelines exist in the codebase; they must not be conflated.”
~/dev/fera/project-design/context/ADR.md:67-95(ADR-003) — one flat credential list::81“A root Ed25519/SSH keypair (ours) is embedded in the launcher binary.”:84-85“We publish a signed manifest (module name → hash → signature) as plain JSON”:86-87“Users manage a local trusted-key list via an in-app UI: name + URL or hex-encoded public key, addable/removable.”
~/dev/fera/project-design/context/ADR.md:817-846(ADR-021) — manifest entry schema:host{...},target{...},artifacts[],fallback,depends_on+layer, and exactly onesignature: { algorithm, key_id, value }. No key authority class, no tier field.~/dev/fera/project-design/context/ADR.md:96-115(ADR-004) — hash mismatch ⇒ non-blocking warning, load proceeds~/dev/fera/project-design/context/QA.md:121-124— QA gate mandates a test for both halves: “a non-blocking UI warning and the module still loads”~/dev/fera/project-design/context/QA.md:135-153— the trust gates cover manifest tamper, full-entry coverage, and downgrade. None of them covers key authority class.
Substrate
Section titled “Substrate”ADR-002’s core insight is correct and important: native dylibs are fast and unsandboxed, WASM plugins are sandboxed and capability-gated. Those two tiers have wildly different consequences. A compromised WASM plugin is contained by construction. A compromised native module runs with full host privileges and, per my other finding, has no panic boundary either.
But the tier distinction exists only in the loading pipeline, never in
the credential that authorises the artifact. ADR-003 defines a single
local trusted-key list — “name + URL or hex-encoded public key,
addable/removable” — and ADR-021’s entry schema carries a single
signature: { algorithm, key_id, value } with no authority class. There
is no way to express “this key may sign a translations container” versus
“this key may sign the native UI engine”.
The consequence is an authority-ladder collapse. The capability a key confers is determined entirely by which artifact it signs, and that is chosen by whoever writes the manifest — not by the user’s consent act.
Concretely, three paths to unsandboxed native code execution, in increasing order of how little the user would notice:
- Deliberate, arguably intended. User adds a key for a benign
purpose (their own translations container, a friend’s theme). That key
is now equally trusted to sign
ui-engine. Nothing in the UI said the key was also a grant of native-code execution. - Social engineering, and there is no confirmation step. ADR-003’s trusted-key UI is a text field for a hex public key. No warning, no tier preview, no “this key will be able to install native modules” affordance is specified. Adding a key is presented as trusting a translator, because that is the mental model the UI text implies.
- Silent, if ADR-004’s advisory check is ever bypassed by a matching manifest entry. Once the attacker’s key is in the list, they publish a manifest entry for the native UI engine with their own hash. The artifact then matches the manifest, so ADR-004’s warning never fires at all — the module is fully signed, fully “trusted-by-design”, and completely silent. ADR-004’s warning is a fallback for casual tampering; it is not a control against a keyed attacker.
Path 3 is the important one, and it is the direct consequence of ADR-002
being implemented only in the loader. The consequence section says the
two pipelines “must not be conflated” — but the credential layer does
conflate them, because a single key_id grants both.
Aggravating: the advisory hash check is the only artifact-level control
Section titled “Aggravating: the advisory hash check is the only artifact-level control”For a native module the only check the design applies to the bytes on disk is ADR-004’s hash comparison, which is deliberately non-blocking and QA-tested to confirm the load proceeds anyway. So for native modules the artifact-level integrity control is a warning, and the only fail-closed control lives one level up, in the manifest signature. That is a defensible design if and only if the manifest is unforgeable — which reduces the whole native-module security story to the integrity of a user-editable key list entered as a hex string.
Options (tradeoffs)
Section titled “Options (tradeoffs)”- Add a key authority class to the trust store —
KeyClass { TranslationsOnly, NativeCode }— and require explicit consent forNativeCode, with the consent UI naming the consequence (“this key can install modules that run with full access to your data”). Keep the common case (translations/themes) frictionless. Cost: one field in the store, one extra enum in the manifest schema, a migration for existing entries. - Separate the two key stores entirely — one read-only keyring for data containers, one small system-only keyring for native modules, the latter not user-extensible (or requiring a stronger gesture such as re-entering an app password). Cost: more UI, more friction, breaks the “users manage their own keys” flexibility ADR-003 wants.
- Tier-scoped manifest signatures: the entry declares which tier it
belongs to and the loader refuses to load a
NativeCodeentry signed by aTranslationsOnlykey, regardless of which pipeline discovers it. Cost: needs the cross-check in both loaders (the thing ADR-002 warns must not be conflated), i.e. shared trust code. - Keep one key list, add an audit trail and a confirmation dialog. Cheapest; does not remove the escalation, only makes it observable. Insufficient on its own.
Proposed solution
Section titled “Proposed solution”Option 1 + Option 3 together:
- Extend the trust store with a key authority class, defaulting new
user-added keys to
TranslationsOnly(safe default; matches the stated use case of “their own or a third party’s key” for containers). - Add an explicit, separately-confirmed
NativeCodegrant, and make the consent text name the blast radius — no sandbox, full host access. - Make the authority class part of the signed manifest entry (ADR-021’s
schema), and have both loaders cross-check
key authority class == artifact tierbefore load. This is the piece that actually closes it: it must be shared code, which is exactly the “must not be conflate” rule from ADR-002 applied to trust rather than to loading. - Add a QA gate next to
QA.md:135-153: “a key added as TranslationsOnly cannot install a native module; the load is refused and names the authority mismatch.” - Note in ADR-004 that its advisory hash check is not the native-tier integrity control, and point at where the real one is — otherwise the two decisions read as if either one alone secures native modules.
Open question for Nika: is the native tier intended to be user-key-trustable at all, or only root-key-signable? Option 2 is the honest answer if not, and it is much cheaper to decide now than after the in-app keyring ships.