TECHSPEC §7 specifies two mutually incompatible trust-manifest schemas, 11 lines apart
Источник:
dev/fera-research/docs/oc3/003-manifest-two-incompatible-schemas.md(read-only; сгенерировано scripts/sync-docs.mjs)
TECHSPEC §7 specifies two mutually incompatible trust-manifest schemas, 11 lines apart
Section titled “TECHSPEC §7 specifies two mutually incompatible trust-manifest schemas, 11 lines apart”- kind: problem
- area: trust
- severity: high
Evidence
Section titled “Evidence”Both claims are in the same file, in the same section, ~11 lines apart.
Schema A — project-design/context/TECHSPEC.md:361-364:
**Trust manifest (`trust` crate):**- Format: JSON (chosen over RON for external-tooling interoperability, ADR-003) — `{ modules: [{ name, version, hash, signature }], ... }`, itself signed with the root Ed25519/SSH key.Schema B — project-design/context/TECHSPEC.md:371-390:
**Versioned manifest entry (ADR-021):** entries carry full version andtargeting detail, hardened:
```json{ "name": "storage", "module_version": "1.4.2", "api_edition": 3, "abi_version": 7, "host": { "min_version": "0.4.0", "max_version": "0.9.99" }, "target": { "triple": "x86_64-pc-windows-msvc", "runtime": "msvc", "features": ["avx2"] }, "artifacts": [ { "file": "storage-1.4.2-x86_64-pc-windows-msvc.dll", "sha256": "...", "size": 4812390 } ], "fallback": { "module": "storage", "builtin": true }, "depends_on": ["shared-types"], "layer": "impl", "signature": { "algorithm": "ed25519", "key_id": "fera-official-2026", "value": "..." }}**Field-by-field divergence:**
| Concept | Schema A (`:363`) | Schema B (`:374-390`) ||---|---|---|| version | `version` | `module_version` (+ separate `api_edition`, `abi_version`) || artifact hash | `hash` (flat, one artifact implied) | `artifacts[].sha256` (+ `.file`, `.size`; array for CPU-feature variants) || signature | `signature` (flat) | `signature: { algorithm, key_id, value }` (nested object) || host compatibility gate | — | `host: { min_version, max_version }` || platform targeting | — | `target: { triple, runtime, features }` || fallback | — | `fallback: { module, builtin }` || dependency graph | — | `depends_on: []` + `layer` |
Schema A has **no** host/target/fallback/depends_on/layer fields. Schema B has **no** `hash`. An entry satisfying A cannot satisfy B, and the two have incompatible field *names* for the two concepts they share.
**The signature covers "the whole entry in canonical serialization" — and canonical serialization is nowhere defined.** `ADR.md:843-845` and `TECHSPEC.md:392-394` both require it. A grep for `canonical` across `ADR.md`, `TECHSPEC.md`, `RFC.md` returns **exactly those two lines** — both are the *requirement*. No scheme is named (no RFC 8785/JCS, no key-ordering rule, no number encoding, no whitespace rule).
**Related absences in the same block:**
- **No trust-on-first-use story.** `grep -ni "trust.on.first\|TOFU"` over the whole `context/` tree returns **zero hits**. `ADR.md:82-84` specifies only a user-managed key list; nothing says how a key gets into it on first sight of a plugin.- **No revocation-list format.** `ADR.md:849` requires "Key revocation, keyed by the existing `key_id`" — no schema, no distribution, no expiry.- **No root-key rotation path.** `ADR.md:78` embeds one root key at build time; there is no defined way to ever change it without shipping a new launcher binary (which `ADR.md:394` says should "change rarely").- **No threshold/N-of-M signing.** `grep -i "threshold sign"` → 0 hits. "TUF-like" appears only as an adjective (`ADR.md:49,77`); there is no snapshot/timestamp split and no freeze mechanism (`grep -i freeze` → 0 hits).
**And the two crates that must agree on this format are both empty:** `core/crates/trust/src/lib.rs` = 8 lines (doc comment + `// Placeholder`), `tools/manifest-signer/src/main.rs` = 12 lines (`println!("fera-manifest-signer: pre-implementation skeleton")`).
## Суть
The trust manifest is, by the project's own assessment, the real security boundary — `ADR.md:110-113`: "The trust manifest signature (ADR-003) is the real security boundary against supply-chain tampering of the *manifest itself*". `QA.md:133-135` requires signature verification "before any of its contents (including the update endpoint) are trusted". `ADR-013` motivates the whole design with a concrete exploit: Discord's unsigned/user-editable `settings.json` update endpoint was abused by unofficial tools to redirect module downloads (`ADR.md:388-391`).
You cannot implement a signed boundary against a schema that the spec states two incompatible ways, and you cannot implement signature verification against a "canonical serialization" that is required but never specified. The signer and the verifier are separate crates in separate processes, written by possibly different people at different times, and nothing in the corpus pins them together. They can each be "correct" and still disagree on bytes.
The fields Schema B omits are exactly the fields that make the manifest a *trust* mechanism rather than a download list. Without `host.{min,max}_version` there is no compatibility gate; without `fallback` an incompatible module has nothing to degrade to (`QA.md:126-128`); without `layer` + `depends_on` there is no load order (`QA.md:157-159`); without `target.triple`/`features` there is no CPU-variant selection (`QA.md:155-156`). Schema A is a download list. Schema B is a trust manifest. The spec currently mandates both.
Separately, the missing TOFU story and missing root rotation mean that even a perfectly-signed manifest gives no answer to "how does a user trust a *third-party* key the first time" or "what happens when the root key is compromised". `ADR.md:91-92` defers web-of-trust to post-v1, but deferring web-of-trust is not the same as having a first-run story for a single key.
## Варианты решения (с tradeoffs)
**Option A — delete Schema A from TECHSPEC §7 (keep only the ADR-021 entry).**- Cost: one line of docs. Any code written against Schema A must be discarded — but there is no code.- Benefit: removes the ambiguity permanently. Schema B is a strict superset of Schema A's intent and already `Accepted` (`ADR.md:819`).- Risk: none. Schema A appears to be a leftover from the pre-hardening sketch.
**Option B — keep both, declare A a deprecated shorthand for B.**- Cost: ongoing ambiguity; two readers, two implementations.- Benefit: preserves the compact example.- Risk: high. The names genuinely differ (`version` vs `module_version`, flat vs nested `signature`), so a "shorthand" would still be a second format.
**Option C — pick a canonicalization and pin it (needed under every option above).**- Cost: a decision with long-lived consequences. JCS/RFC 8785 is battle-tested but its canonical output is not what most JSON serializers emit by default, so both the signer and every third-party verifier must implement it deliberately.- Benefit: makes `ADR.md:843` implementable, and makes `QA.md:149-151` ("a tampered-but-unsigned modification of any field … is rejected") testable.- Risk: getting it subtly wrong is exactly the failure the ADR is trying to prevent.
## Предлагаемое решение
**A + C, as one spec-level decision, before either crate gets its first line of code.**
1. **Delete the `{ modules: [{ name, version, hash, signature }] }` line at `TECHSPEC.md:363`**, replacing it with a pointer to the ADR-021 entry schema at `:374-390`. Schema A is superseded by construction; keeping it is pure liability.2. **Define canonical serialization explicitly.** Recommend: the signature is computed over the RFC 8785 (JCS) canonicalization of the entry object with the `signature` field **absent**, hex-encoded, then signed. Rationale — JCS is the only widely-implemented scheme that is (a) byte-deterministic across languages, (b) already normalized for key order/number form, and (c) implementable in Rust without a second crate dependency graph. Write it into `ADR-021` as a sub-clause next to `ADR.md:843-845`.3. **Decide the first-run story now, even if the answer is "there isn't one."** For a closed alpha (a small trusted circle, `NAMING-UX.md:78-79`) the honest answer is likely: only the root key is pre-trusted; third-party keys are added by pasting a hex key through the in-app UI (`ADR.md:82-84`), and the UI states plainly that the user is trusting it on their own. Recording that as the decision is worth more than leaving it implicit, because `QA.md:136-137` (add/remove trusted key, takes effect without restart) is written against it.4. **Add root-key rotation to the ADR or explicitly defer it.** The "defer" is fine for a closed alpha; the silence is not — a root key with no rotation path is a single point of total compromise.
## Cross-references
- `docs/oc3/005-launcher-trust-plugins-are-skeletons.md` — the crates that would consume this schema- This is the kind of spec-hygiene item that `QA.md:188-189` ("no orphaned TODOs") is meant to catch, and that gate currently has no automated form. See also `docs/oc3/010-spec-hygiene-defects.md`.