# The pinned bundle — verifying the whole web client from one shell

> **Status: DESIGN, not built.** Live today: the egress-lock CSP, the signed, reproducible **render** build, `/cert.json`,
> and CSP-sha256 pinning of the shell's **inline** script. What this doc proposes and is **not** yet shipped: extending
> that pin to the shell's **imported module closure**, via a bootstrap loader + a signed client manifest. It is the
> web-side complement to [the attested client](./the-attested-client.md) (the native path) and the piece that lets the
> reproducible-build proof ([verify](./verify.md) Tests 6–7) cover the code the browser **actually executes**, not just
> the render and the inline shell.

Every proof on this site about *our* side — reproducible build, egress-lock, signed `/cert.json` — is strong. The client
has a smaller, quieter gap that this closes.

## The gap, precisely

The `/space` page is a small HTML **shell**: an inline app module (which the CSP hash-pins — the browser enforces it
matches a declared `sha256`) plus `import` statements pulling in ~23 sibling modules — `e2ee.js` (your encryption),
`recovery.js`, `backupVault.js`, `deviceLink.js`, and the rest. Those imports pin the **names** of what loads, not the
**bytes**. The modules load under `script-src 'self'` — *same-origin, but not content-pinned* — and the CSP hashes cover
only the inline blocks. So:

- **A swapped module survives a shell check.** Serve the *identical* shell (its hash still matches the published good
  one) but a tampered `e2ee.js`, and a shell-only verification passes while the code doing your encryption is the
  attacker's.
- **The egress lock does not catch it.** A malicious `e2ee.js` can't reach `evil.com` (CSP blocks it), but it *can* fold
  your key into a request to an **allow-listed** host, or simply **weaken the crypto** so the ciphertext the server
  already stores becomes readable — no forbidden egress at all.
- **An external `curl | sha256sum` verifies a *different* fetch.** The checker's download and the browser's download are
  two separate requests; a server can serve a good copy to the checker and a bad copy to you (*split-view / targeted
  delivery*). The external check proves something about *a* download, not *your* session.

So today's reproducible-build proof faithfully covers the render (the Lambda) and the inline shell, but the **module
closure the browser runs** is origin-trusted, not content-verified.

## This is not self-attestation — it is a bootstrap

A page **cannot** verify *itself*: any check a page runs on its own code can be hooked or spoofed (this is why
[the attested client](./the-attested-client.md) reaches for a native, hardware-attested shell). This design does **not**
claim otherwise. It does something narrower and achievable: it **collapses the client's verifiable surface from ~23
moving files to one small, stable shell**, by having the shell content-pin everything it loads. Whatever anchors that one
shell then covers the whole client. It is **measured boot for the web** — a small verified core that measures each next
stage before running it.

## The mechanism

1. **A signed manifest.** The build emits `{ path: sha256 }` for the transitive module closure and signs it into
   `/cert.json` alongside the existing `gitCommit` / `lambdaCodeSha256` / `egress` — new fields `shellSha256`,
   `closureSha256`, and `modules[]`. Hashes are computed **after** the `?v=` stamp, so they are the served bytes (the
   same discipline `gen-space-csp` already uses for the inline hashes).
2. **A loader in the shell.** The shell replaces direct `import './x.js'` with `loadVerified('./x.js')`:
   `fetch` once → `sha256` → compare to the manifest → **execute that exact copy** via a `blob:` module URL → otherwise
   **abort (fail-closed)**. It is a few auditable lines at the top of the shell.
3. **One fetch, verify, run — never re-fetch.** The trap to avoid: "fetch a copy to hash, then `import` a fresh copy"
   triggers a *second* fetch and reopens split-view at the module level. The loader must run the same bytes it hashed.
   (This needs `blob:` added to `script-src`; `connect-src` — the egress lock — is unchanged.)
4. **Reproducibility extends to the client.** Rebuilding the published source yields the same manifest and the same
   served module bytes — so [verify](./verify.md) Tests 6–7 grow from "the render reproduces" to "the render **and the
   whole client closure** reproduce."

A swapped `e2ee.js` now **fails to load** rather than silently running.

## Anchoring the one shell — the honest residual

The shell still cannot certify itself; its integrity must be anchored from **outside**, and that is now a single small,
stable file instead of a moving set. In increasing strength:

| Anchor | What it gives | Trust root |
|---|---|---|
| Browser **CSP sha256** on the inline shell | self-consistency (the shell matches a *declared* hash) — not goodness | the browser |
| Signed **`/cert.json`** shell hash | an external/browser checker verifies **one** hash against a published key | the signing key |
| **Transparency log + gossip** of the shell hash | split-view of the shell becomes *detectable* by any one honest observer | independent logs (see [trust model](./trust-model.md)) |
| **Native attested shell** ([the attested client](./the-attested-client.md)) or a verifier **extension** | verifies the bytes the browser *actually loaded* — closes split-view | hardware / browser vendor |

This doc closes the **module** gap and shrinks the anchor to **one shell**; anchoring that one shell end-to-end is the
job of the native attested client, a verifier extension, or transparency gossip — each buildable independently.

## Why the naive alternatives don't suffice

| Alternative | Why it fails |
|---|---|
| Hash the shell only | pins *names*, not the modules' *bytes* → module swap |
| `script-src 'self'` | *origin*, not *content* — any same-origin script is allowed |
| SRI on `import` | ES modules can't carry an integrity attribute today (import-maps-with-integrity is emerging, not enforceable) → the shell must pin in code |
| External `curl | sha256sum` | a *different fetch* → split-view; verifies not-your-bytes |
| In-page console hash | runs in the page's own world → a bad page can hook `fetch`/`crypto` |

## What it proves — and what it doesn't

| | Status |
|---|---|
| The **executed module closure** matches a signed manifest reproducible from published source | ✅ **once built** (this design) |
| A swapped module **fails to load** instead of silently running | ✅ **once built** |
| Client verification is reduced to **one shell hash** anyone can anchor | ✅ **once built** |
| The shell's *own* bytes are pinned against a malicious server / split-view **by itself** | ❌ needs an external anchor — browser CSP + signed hash today; gossip or a native/extension verifier to fully close |
| Protection against a genuine **bug** in the honest code | ❌ orthogonal — receiver hardening still matters |

## Roadmap fit

- **Live:** egress-lock CSP, signed reproducible render build, `/cert.json`, CSP-hashed inline shell.
- **This doc (proposed):** the bootstrap loader + signed client manifest → the whole client closure becomes **one
  verifiable unit**.
- **Then:** anchor that one shell end-to-end — transparency-log gossip on the shell hash, and/or
  [the attested client](./the-attested-client.md) / a verifier extension; plus enclave attestation on the server
  ([the attested tier](./the-attested-tier.md)). The full target composition — for a power user on Firefox — is
  [the power-user verification stack](./the-power-user-stack.md).

Honest position: this is the highest-leverage single step for *web* client integrity — it turns "trust ~23 same-origin
files" into "anchor one small, signed, gossip-able shell," which is exactly the shape every stronger anchor (gossip,
extension, native attestation) can actually get its hands around.
