# OBEP Security Model

This document states the trust model plainly so a customer's security team can
audit it from the open `/obep` code alone. The governing rule: **anything whose
secrecy would create a vulnerability is in open OBEP; anything whose secrecy only
protects the business may be in closed OpenErrand.** The proof the split is
honest: *even a fully malicious OpenErrand relay cannot read a vault or widen a
playbook, because the open extension re-verifies everything.*

## Trust anchors (all open, all in `/obep`)

- **Playbook signing/verification** — Ed25519 over RFC 8785 (JCS) canonical JSON.
  The tenant signs; the relay holds only the public key; the extension
  re-verifies the signature **and** the content hash against a locally-trusted
  key before executing. ([protocol](../obep/protocol/src/), [extension enforcer](../obep/extension/src/background/enforcer.ts))
- **Permission enforcement** — default-deny, per-action, client-side, identical
  for deterministic steps and LLM-fallback commands. ([enforcement](../obep/enforcement/src/engine.ts))
- **The wire protocol + token formats** — pairing tokens, identity assertions,
  and task tokens are all open and verifiable. ([wire](../obep/protocol/src/wire.ts), [token](../obep/protocol/src/token.ts))
- **The relay protocol contract** — encoded as the runnable [conformance suite](../obep/conformance/).

## "Even a malicious relay cannot…"

| Attack | Why it fails |
|---|---|
| Swap in a wider playbook | Extension recomputes the hash and re-verifies the tenant signature; a widened body fails both. |
| Re-sign a widened playbook | The relay never holds the tenant private key; a different key fails the extension's check. |
| Read a credential | `fillSecret` carries only a vault *key reference*; the value is AES-GCM encrypted on-device and resolved at the moment of use. The relay never sees it. |
| Route across tenants | `tenantId` is derived from the app's authenticated API key, never from a message; routing requires a matching `(tenantId, userId)` binding. |
| Read another tenant's audit | `/audit/:tenantId` requires that tenant's API key (cross-tenant ⇒ 403). |
| Replay a task/pairing token | Tokens are single-use (nonce-tracked) and short-lived (`exp`). |

## Layered defenses for "no sensitive data leaves" (in order of strength)

1. **Capture minimization (strongest).** Default is the stripped interactive-element
   list — labels/refs/types, **no values, no screenshot**. No payload ⇒ nothing to leak.
2. **Playbook domain allowlist.** Can't reach a surface ⇒ can't capture it. Hard stop.
3. **Egress lock.** The extension's `connect-src` CSP permits only secure transports
   (`https:`/`wss:`, plus `localhost` for dev) — never cleartext remote origins — and the
   code opens exactly one connection: to the relay (and, if configured, your decider)
   endpoint **you** set. The single reachable endpoint is fixed by *configuration*, not by
   the CSP host list; lock it down in fleet deployments via managed-config pinning
   ([ENTERPRISE_DEPLOYMENT.md](./ENTERPRISE_DEPLOYMENT.md)).
4. **Local redaction (layer 4, best-effort).** Regex + Luhn + entropy over labels/DOM
   *before transmission*. Catches structured PII/keys on allowed pages we didn't
   anticipate. **Not a guarantee** — unstructured PII (names) needs NER and is out of
   scope. ([redact](../obep/enforcement/src/redact.ts))
5. **Dry-run recorder + audit.** Developers see leaks before launch; runtime audit logs
   *that* a capture occurred (domain + hash), never the content.

Redaction is layer 4, not layer 1. Capture minimization and the allowlist are what keep
secrets in; redaction is the safety net. Customers must not treat redaction as a guarantee.

## Credential vault

- AES-GCM via Web Crypto, key derived from a user passphrase (PBKDF2, 210k iters),
  encrypted **before** anything touches `chrome.storage.local` (never `sync`).
- Namespaced per binding, with the binding key as AES AAD ⇒ cross-binding reads fail
  cryptographically.
- Decrypt-at-moment-of-use; the key lives only in service-worker memory.
- We cannot decrypt a vault server-side and cannot recover a lost passphrase — by design.

## Extension hardening

- Outbound-only WSS; reconnect with exponential backoff + jitter.
- **Sender validation**: a relay-signed, single-use task token is verified against the
  relay's public key before the extension acts — a message is never trusted just because
  it arrived on the socket.
- Only touches tabs it opened; no remote code, no `eval`; strict CSP.
- Kill switch detaches the connection, aborts tasks, and locks the vault instantly.

## What we deliberately cannot do

- Decrypt a user's vault server-side (we never hold the key).
- Recover a lost vault passphrase (offer re-entry, not a backdoor).
