Ctrl K
ComunidadGuías…

docs/THREAT_MODEL.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Threat Model

This document describes the trust boundaries, adversary model, and security assumptions
for the Crux Daemon.

## Trust Boundaries

```
                       +-----------+
   Untrusted           |  Client   |      HTTP / gRPC
   (external)          +-----------+
                             |
   ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ | ─ ─ ─ ─ ─ ─ ─ TLS termination boundary
                             |
   Trusted (internal)        v
                       +-----------+       +-------------+
                       | corecruxd |------>|  Data dir   |
                       |  (daemon) |       | (segments)  |
                       +-----------+       +-------------+
                             |
                             v
                       +-----------+
                       | CROWN     |
                       | receipts  |
                       +-----------+
```

### Boundary 1: Client to Daemon

- **Network:** The daemon binds to `127.0.0.1` by default. Non-loopback binding with
  `DevScopes` or `Off` auth modes is blocked unless `CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1`
  is explicitly set.
- **TLS:** The Crux Daemon does **not** implement TLS natively. Deployments exposed
  beyond localhost **must** use a TLS-terminating reverse proxy (Caddy, nginx, Envoy).
- **Auth:** Controlled by `CORECRUXD_AUTH_MODE` (required, no default):
  - `off` - No authentication. All scope checks bypassed. Use only for local development.
  - `dev_scopes` - Scopes provided by client via HTTP headers. No cryptographic verification.
    Emits a startup warning. Suitable for development and testing only.
  - `jwt_hs256` - HS256 JWT verification with `exp`, `nbf` validation and 30s leeway.
  - `jwt_jwks` - JWKS/OIDC JWT verification. Production-recommended.

### Boundary 2: Daemon to Storage

- Segments are stored as files in `CORECRUXD_DATA_DIR`.
- BLAKE3 hashes ensure **integrity** (tamper detection), not **confidentiality**.
- Data at rest is **not encrypted**. Use filesystem-level encryption (LUKS, dm-crypt) if
  required by your threat model.
- Daemon control records (passports, grants, work/gates, coordination, receipts,
  tenant metadata, and related internal state) occupy reserved fact namespaces.
  Generic HTTP/MCP writes and deletes, candidate promotion, extension/WASM
  writes, remote sync, and CruxPack import reject those namespaces. Only the
  owning typed daemon workflow may write them through the low-level store.
- The canonical namespace policy lives in
  `corecrux_memory::fact_privacy::{DEFAULT_PRIVATE_PREFIXES,
  DAEMON_OWNED_ENTITY_PREFIXES, GENERIC_CREATE_RESERVED_PREFIXES}`. Control
  records remain born-private even if a runtime sharing override names their
  prefix. The `__agent::<owner>::` prefix is a daemon-assigned physical wrapper:
  clients mutate the owner-visible logical entity and cannot create the wrapper
  directly.
- This boundary is prospective. Reclassifying a legacy row as private during
  replay does not authenticate who originally wrote it. Operators upgrading a
  store that may have accepted generic writes into a control namespace must
  inventory and reissue those rows through their typed governance API before
  relying on them as authoritative.
- Engram overlays are control records: generic fact writes cannot address
  `__engram__::`; authenticated `PUT /v1/engrams/{name}` with `admin:write`
  validates the typed object and stamps daemon-owned actor, time, tenant,
  privacy, and provenance fields.
- JWT modes default wired HTTP fact-backed surfaces to real tenant
  stamping/filtering. Multi-tenant and wildcard tokens on tenant-implicit
  routes require an authorized `X-Corecrux-Tenant-Id`; an explicit route/body
  tenant also selects the tenant and must agree with the header. Missing,
  ambiguous, or mismatched claims fail closed. Operators may explicitly set
  `CORECRUXD_TENANT_WRITE_STAMP=off` or `shadow` only while migrating
  historical shared-`default` rows. This flag does not cover MCP or stores with
  independent entity/session/projection tenant models.
- Decision-tool rows deliberately remain compatibility annotations. Their
  BLAKE3 value is a content identifier, not a signature or append-only proof;
  consumers must require the `integrity: "untrusted_annotation"` contract and
  must not use these rows as authorization decisions.

### Boundary 3: CROWN Receipts

- Ed25519 signatures provide non-repudiation and tamper evidence for appended events.
- The signing key is loaded from `CORECRUXD_RECEIPTS_KEYRING_PATH` or
  `CORECRUXD_RECEIPTS_KEYRING_JSON`.
- Key material is held in process memory. Compromise of the daemon process exposes the key.
- Store verification is performed by `corecruxctl verify-store`; CROWN receipt signature
  verification is performed by the receipt verifier and the `/v1/receipts/` API endpoints.

## Adversary Model

### What CROWN receipts protect against

- **Post-write tampering:** Any modification to sealed segments invalidates the BLAKE3 chain
  and Ed25519 signatures.
- **Selective omission:** The chain structure makes it computationally infeasible to remove
  events from sealed segments without detection.
- **Receipt forgery:** Without the Ed25519 private key, an adversary cannot produce valid
  receipts for fabricated events.

### What CROWN receipts do NOT protect against

- **Key compromise:** If the signing key is exfiltrated, all future receipts can be forged.
  Rotate keys and audit access to keyring files.
- **Pre-seal tampering:** Events in the active (unsealed) head segment are not yet covered
  by the sealed chain. The window is bounded by the seal interval.
- **Side-channel attacks:** The daemon does not implement constant-time signing; timing
  attacks on the Ed25519 operations are not mitigated.

## Auth Mode Implications

| Mode | Verification | Production-safe | Use case |
|------|-------------|-----------------|----------|
| `off` | None | No | Local development, air-gapped |
| `dev_scopes` | None (header pass-through) | No | Integration testing |
| `jwt_hs256` | Symmetric JWT (exp/nbf/iss/aud) | Qualified | Small deployments with shared secret |
| `jwt_jwks` | Asymmetric JWT via JWKS/OIDC | Yes | Production with identity provider |

## Network Assumptions

1. The daemon assumes the network between reverse proxy and daemon is trusted
   (loopback or private network). It only consumes `Forwarded` /
   `X-Forwarded-For` for rate-limit keying from peers listed in
   `CORECRUXD_TRUSTED_PROXY_CIDRS`.
2. gRPC replication between nodes should use authenticated channels
   (`CORECRUXD_REPLICATION_AUTH_BEARER`).
3. Prometheus metrics (`/metrics`) and health endpoints (`/healthz`, `/readyz`) are
   unauthenticated. Restrict access at the network level if exposing beyond localhost.
4. MCP Streamable HTTP requests fail closed whenever either registered bearer
   tokens or hosted-client OAuth introspection is configured. This includes
   server-info GET, SSE stream creation, and JSON-RPC POST; only an MCP daemon
   with neither authentication rail configured permits anonymous access.
5. Public `/v1/version` is redacted. Full operational version details live at
   `/v1/admin/version` behind `admin:read`.
6. `GET /v1/receipts/signing-keys` is unauthenticated and serves only the
   receipt-signing *public* key. It is discovery, not trust: a key fetched
   from the daemon that signed a receipt proves nothing if that daemon or the
   channel is compromised. Verifiers pin the key at enrolment (or cross-check
   a witness) and treat a changed key as an alarm.

## Capability Token Trust and Revocation

- **Local-token trust invariant.** The capability router (`crux-router`) skips
  signature verification for the `local` backend. This is sound only because the
  token reaching the router is daemon-minted (self-minted local token in
  `corecruxd` startup) and never client-injected — the local token does not
  cross a trust boundary. Hosted/customer backends are always signature-verified
  against a configured trusted issuer key; the local short-circuit cannot be
  leveraged to authorise a hosted lane. A future change that routes a
  client-supplied token through the router must construct it with a trusted
  issuer pubkey. This is pinned by the
  `local_signature_bypass_does_not_extend_to_hosted_backend` regression test.
- **Revocation is modelled but not yet enforced.** Tokens carry `crl_url` and
  `push_channel` revocation hints, but the router does not yet consult them, so a
  revoked-but-unexpired token is still authorised within its validity window.
  To avoid misleading downstream auditors, the router mode stamp carries
  `revocation_checked: false`; an authorised decision does **not** imply the
  token was checked against a CRL or revocation timestamp. Mitigation today:
  keep token lifetimes short. Revocation IO (CRL/timestamp consult) is a planned
  later phase.

## Key Escrow and Recovery

Scope: the `crux-escrow` crate. A vault's data encryption key (DEK) never leaves the
customer's control in plaintext. This section is the M0 artefact of the key escrow and
recovery plan and is the contract the crate's tests assert against.

### Recovery modes

| Mode | What the server holds | Reconstructs the DEK | Customer artefacts |
|---|---|---|---|
| **Layer 0 — recovery code** (default) | wrapped DEK ciphertext only | the recovery code alone | one 256-bit code, shown once |
| **Layer 1 — Shamir 2-of-3** (opt-in) | wrapped DEK ciphertext + **share C** | any **two** of A/B/C | device share A, printed share B |

Layer 0 is not escrow: we hold ciphertext and no key-derivation input. Layer 1 adds one
share, deliberately one short of a threshold.

### Adversaries

| Adversary | Capability assumed | Defeated by | Residual |
|---|---|---|---|
| **Us (honest)** | full read of every server-side store | share C is 1 of 3 in a 2-of-3 scheme; Layer 0 stores no key input at all | none — insufficiency is arithmetic, not policy |
| **A breached us** | offline copy of everything at rest, including share C | same threshold argument; share C is stored non-exportable and its release is an operation, not a read | an attacker who *also* phishes share A or B reconstructs. Two independent compromises required |
| **An account thief** (valid passport, no device) | authenticated as the account holder | release of share C is delayed (default 72h), notified to every registered device, and cancellable by any of them | a thief who holds the account **and** all registered devices **and** waits out the window succeeds. This is the account-recovery floor for any online system |
| **A network observer** | full transcript of client↔server traffic | only wrapped ciphertext and share-C *release decisions* cross the wire; unwrapping is client-side | traffic analysis reveals *that* a recovery occurred, not the key. Metadata is receipted anyway |
| **A coerced operator** (court order, insider under duress) | can compel production of everything we hold | share C alone reconstructs nothing; there is no server-holds-the-key mode to compel | we can be compelled to hand over a useless share, and to say so. Published in the M5 transparency statement |
| **A malicious client** | crafted shares, replayed or forged release requests | shares are integrity-tagged (corruption is detected, not silently mis-reconstructed); release requests are passport-bound and receipted | a client that already holds 2 valid shares is the legitimate owner by construction |

### Constraint mapping

Each constraint from the plan maps to a named defence, or is an accepted risk:

1. **Boring** — CSPRNG (`rand`), BLAKE3 in KDF mode (`derive_key`) to turn the recovery
   code into a wrapping key, XChaCha20-Poly1305 AEAD to wrap the DEK, and a published
   Shamir implementation over GF(2^8). All four are existing workspace dependencies or a
   vetted addition; no novel construction.

   A password KDF (Argon2id, scrypt) is deliberately **not** used. The recovery code is 256
   bits of CSPRNG output, not a user-chosen password: there is no low-entropy input for a
   memory-hard function to defend, and it would only add a dependency and a tuning
   parameter. This is the same reasoning recorded for the relay device-credential secret.
2. **Insufficient by construction** — enforced by the threshold, and asserted by the
   `one_share_yields_nothing` and `server_dump_yields_nothing` tests. Neither test can
   pass if the server's holdings ever become sufficient.
3. **Redundant without weakening** — share C may be replicated freely (Vault + sealed
   offline backup) *because* it is insufficient alone. Replication count does not appear
   in any security argument, so operational care is not load-bearing.
4. **Receipted** — every wrap, escrow opt-in, release request, notification, cancellation
   and completion emits a CROWN receipt into the customer's own timeline.

### Unrecoverable by design

**Losing both user shares (A and B, or the Layer 0 recovery code) is unrecoverable.**
This is not a gap to close later. The only way to make it recoverable is for our holdings
alone to reconstruct the DEK — which is Constraint 2 inverted, and would mean a breach of
us is a breach of every customer's data. We accept permanent loss in the rare case in
order to make catastrophic loss impossible in the common one.

Product consequence: the customer must be told this *before* opting in, in the UI, not in
a help article afterwards. That is the M6 launch gate.

### Not defended against

- **A compromised client device at wrap time.** If the endpoint that generates the DEK is
  owned, escrow is irrelevant — the plaintext is already there.
- **A user who photographs their recovery code into cloud storage.** Out of scope; the
  code is rendered for transcription, with print/download offered once.
- **Rubber-hose against the *customer*.** They hold 2-of-3 by design; they can be compelled
  to use it. No key-holder scheme defends against this.

## Error Response Policy

Error responses for shard routing errors (`SHARD_UNAVAILABLE`, `WRONG_SHARD`,
`SHARDMAP_VERSION_MISMATCH`) are sanitised by default and do not include internal topology
details (gRPC addresses, shard map versions). Set `CORECRUXD_DEBUG_ERRORS=true`
to include full details for debugging.

## Rate Limiting

The Crux Daemon implements coarse client-IP rate limiting and request caps.
Use your reverse proxy (Caddy `rate_limit`, nginx `limit_req`, etc.) for
route-specific resource protection. `X-Corecrux-Passport-Id` is not trusted as
a pre-auth rate-limit key; unauthenticated callers cannot rotate it to obtain
independent buckets.

Global request bodies default to 16 MiB. Bulk/import endpoints that need a
larger envelope have explicit route-specific limits. The console embedding
probe is an authenticated admin write operation and rejects metadata,
link-local, private, multicast, unspecified, and DNS-rebound targets by default
unless the target matches the configured embedding endpoint or an explicit
local-probe override is set.

## Route Authorization Proof

The CI test gate `route_auth_matrix_is_complete` parses the live Axum router
source and fails when a route lacks one of these classes: public, read, write,
admin read, admin write, internal replication, or feature-gated. Companion
tests pin representative scope contracts and high-risk HTTP boundary routes.

## Dependency Security

- `cargo deny check` is run in CI to detect known advisories, bans, licence
  drift, and source-policy violations.
- `cargo audit` is run in CI as a secondary RustSec advisory check. It
  currently surfaces the yanked `aes 0.9.0` warning through `zip 8.6.0`; this
  is visible but non-blocking until the upstream dependency path can move.
- Known RustSec ignores in `deny.toml` must carry owner and expiry comments; CI
  enforces this metadata.
- Container images are scanned with Trivy before push. Emergency skips require
  a structured waiver with owner, expiry, reason, commit SHA, run ID, and image
  reference, uploaded as a 90-day artifact.
- Parser/verifier fuzz targets run on the scheduled workflow and as bounded PR
  runs when fuzz, frame, receipt, router, or lockfile paths change. Crash and
  corpus artifacts are uploaded for follow-up.
- `cargo deny`'s `wildcards` policy is `deny`: no workspace crate may declare a
  `"*"` version. `multiple-versions` remains `warn` because two RustCrypto
  generations coexist (the stable `digest 0.10` / `der 0.7` stack from
  `ed25519-dalek` and `p256`/`ecdsa`, and the `digest 0.11` / `der 0.8` stack
  from `cms`, `x509-cert`, and `zip`). This duplication is tracked, not silently
  accepted; the crypto subset will move to a targeted `deny` once upstreams
  converge.
- **Pre-release crypto in witness verification.** `corecrux-receipts` depends on
  `cms 0.3.0-pre.2` and `x509-cert 0.3.0-rc.4`. These parse RFC3161 timestamp
  tokens for the optional witness/co-signature path only — they are not in the
  core CROWN Ed25519 receipt signing/verification path, which uses stable
  `ed25519-dalek 2.x`. `cms` is the only Rust CMS/PKCS#7 parser and is still
  pre-release upstream, so it cannot be replaced with a stable equivalent today.
  Treat witness-timestamp parsing as a defence-in-depth signal, not a primary
  trust anchor, until these crates reach a stable release.