Extends the A5 pre-stage now that dev-b's full B-stream (item CRUD + all 19 org subcommands) merged to main (7392795). Living docs: - FORMATS/CRYPTO/SECURITY/DESIGN: flip the item-CRUD "pending Dev-B" markers to shipped; SECURITY audit vocabulary moves item-* actions to live. - crates/relicario-cli/ARCHITECTURE.md: full 19-subcommand surface (12 admin + 7 item CRUD), accurate OrgAddKind scope (Login/SecureNote/Identity). - STATUS.md: enterprise-org-vault landed section (merged7392795) + tracked follow-ups + honest known-limitations; correct spec citation. - ROADMAP.md: backend-complete row + phase-2 follow-ups. - CHANGELOG.md: finalize the enterprise-org-vault Unreleased section (item CRUD into Added; Card/Key/Document/Totp + extension + phase-2 into Deferred). Code (PM-directed dead_code fixes): wire device::current_device_seed by removing the identical duplicate private fn in org_session.rs (de-dup); #[allow(dead_code)] + justification on org_session org_meta_path/load_meta (API completeness, no command consumes org.json yet). Also silence a 3rd pre-existing test-only warning (unused relicario() helper in tests/org_init_signing.rs). Honest deferrals kept explicit throughout: Card/Key/Document/Totp org add/edit parity, extension org switch/read (Dev-D) + writes, phase-2 (SSO/LDAP, read audit, per-collection subkeys, HTTP plane). Full workspace cargo test green, zero warnings. All cited code constants pinned file:line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJo44YM3UbBjro2fG6NrKy
222 lines
10 KiB
Markdown
222 lines
10 KiB
Markdown
# Relicario Security Model
|
|
|
|
> **Audience:** auditors and curious users. This doc owns the threat model, attacker-scenarios table, device-authentication model, env-var trust surface, and known limitations. **Does NOT own:** crypto primitive details (see [CRYPTO.md](CRYPTO.md)), wire formats (see [FORMATS.md](FORMATS.md)), or implementation (see [../crates/relicario-core/ARCHITECTURE.md](../crates/relicario-core/ARCHITECTURE.md) and [../crates/relicario-cli/ARCHITECTURE.md](../crates/relicario-cli/ARCHITECTURE.md)).
|
|
|
|
## Cryptographic Protection
|
|
|
|
Relicario uses two-factor vault decryption:
|
|
1. **Passphrase** — user-memorized, zxcvbn score ≥3 required
|
|
2. **Reference image** — JPEG carrying 256-bit secret via DCT steganography
|
|
|
|
Key derivation: Argon2id (64 MiB memory, 3 iterations, 4 parallelism)
|
|
Encryption: XChaCha20-Poly1305 (192-bit nonce, 256-bit key)
|
|
|
|
## Manifest Integrity
|
|
|
|
The manifest (`manifest.enc`) is encrypted with AEAD, which provides:
|
|
|
|
- **Confidentiality**: Contents unreadable without master key
|
|
- **Integrity**: Any modification detected and rejected on decrypt
|
|
- **Authenticity**: Only master key holders can create valid ciphertexts
|
|
|
|
### What AEAD Does NOT Protect
|
|
|
|
- **Item deletion**: An attacker with write access can delete `.enc` files
|
|
or git-revert commits. The manifest decrypts successfully but won't
|
|
contain the deleted items.
|
|
|
|
- **Rollback attacks**: An attacker can replace `manifest.enc` with an
|
|
older valid version. AEAD accepts any ciphertext created with the key.
|
|
|
|
### Mitigation
|
|
|
|
Item deletion and rollback are detectable via **git history**:
|
|
|
|
```bash
|
|
git log --oneline items/
|
|
```
|
|
|
|
For environments where git history could be rewritten (force-push):
|
|
|
|
1. Enable device authentication (commit signing + pre-receive hook)
|
|
2. Use a git server that rejects non-fast-forward pushes
|
|
3. Regular backups with `relicario backup export`
|
|
|
|
## Device Authentication
|
|
|
|
When enabled, device authentication provides:
|
|
|
|
- **Commit authorship**: All commits signed by registered device keys
|
|
- **Push access control**: Deploy keys managed via Gitea API
|
|
- **Instant revocation**: One command cuts off both signing and push
|
|
|
|
Enforcement requires deploying the `relicario-server` pre-receive hook
|
|
on the vault remote. The crate provides two subcommands:
|
|
|
|
- `relicario-server generate-hook` — emits the hook script to install at
|
|
`<repo>/hooks/pre-receive`
|
|
- `relicario-server verify-commit <sha>` — checks one commit's signature
|
|
against `.relicario/devices.json` and `.relicario/revoked.json` as of
|
|
that commit; the hook calls this for every pushed ref
|
|
|
|
Without the server hook, signed commits provide authorship metadata only
|
|
— any process with push access can land an unsigned commit, since
|
|
verification is otherwise advisory.
|
|
|
|
See `docs/superpowers/specs/2026-05-02-device-authentication-design.md`.
|
|
|
|
## Access Control
|
|
|
|
Without device authentication, access control is transport-layer only:
|
|
|
|
- **CLI**: SSH key authentication to git remote
|
|
- **Extension**: Git credentials in browser storage
|
|
|
|
Device registration is optional but recommended for shared vaults.
|
|
|
|
## Org vault security
|
|
|
|
An org vault is a separate git repository alongside the personal vault. It
|
|
uses ed25519 commit-signing and a server-side pre-receive hook to make
|
|
least-privilege access control server-enforced, not advisory.
|
|
|
|
### Org device-key authentication
|
|
|
|
Every org member registers an ed25519 device key. The key appears in
|
|
`members.json` as an OpenSSH public-key string alongside the member's role
|
|
and collection grants. Fingerprint matching is done via
|
|
`relicario_core::fingerprint`, which normalises the OpenSSH format so that
|
|
whitespace and comment differences do not create phantom mismatches.
|
|
|
|
Org access requires two things at once: a wrapped key blob (`keys/<member-id>.enc`)
|
|
and the device private key that can unwrap it. There is no org passphrase —
|
|
removing a member's blob and rotating the org master key is sufficient to
|
|
revoke access (see **Key rotation** below). Device keys are completely
|
|
separate from the personal vault's KDF inputs; revoking org access does not
|
|
affect the member's personal vault.
|
|
|
|
### Pre-receive hook enforcement
|
|
|
|
`relicario-server generate-org-hook` (`crates/relicario-server/src/main.rs:511`)
|
|
emits a hook script that calls `relicario-server verify-org-commit` for
|
|
every pushed commit. Unsigned or structurally invalid commits are rejected
|
|
before they land.
|
|
|
|
`verify_org_commit` (`main.rs:286`) performs four checks in order:
|
|
|
|
1. **Signature verification** — a temporary `allowed_signers` file is
|
|
constructed from the current `members.json`; `git verify-commit --raw`
|
|
is run and the resulting SHA-256 fingerprint is matched back to a
|
|
`members.json` entry. A commit not signed by a *current* member is
|
|
rejected outright.
|
|
|
|
2. **Path-level write authorisation** — each modified path is classified by
|
|
`classify_path` (`crates/relicario-server/src/lib.rs:19`) into
|
|
`ProtectedJson` (owner/admin write only), `CollectionItem` (the
|
|
`items/<slug>/…` prefix; write allowed only if the slug appears in the
|
|
signer's `collections` grant array), or `Unrestricted`. The write is
|
|
authorised if and only if the signer's role and grants satisfy the
|
|
classification. Item blobs are authorised by the leading path segment
|
|
alone — the ciphertext is never decrypted by the hook.
|
|
|
|
3. **Owner-only elevation guard** (`enforce_owner_only_elevation`,
|
|
`main.rs:438`) — only a member whose *pre-commit* (parent) role is Owner
|
|
may introduce a new member at Owner or Admin level, or promote an
|
|
existing member to either. Checking the pre-commit role means an Admin
|
|
cannot self-promote in the same commit that writes the escalated
|
|
`members.json`; there is no epoch in which the transition is
|
|
self-authorised.
|
|
|
|
4. **Schema monotonicity** (`enforce_schema_monotonicity`, `main.rs:521`)
|
|
— `schema_version` values in org JSON containers may not decrease.
|
|
Merge commits are rejected. A genesis commit (no parents) is allowed
|
|
only when it is signed by the sole Owner it introduces.
|
|
|
|
### Key rotation
|
|
|
|
`relicario org rotate-key` generates a fresh 256-bit org master key,
|
|
re-wraps it for every current member, and re-encrypts every
|
|
`items/<slug>/<id>.enc` blob and the manifest under the new key in a single
|
|
signed commit tagged `Relicario-Action: key-rotate`. A revoked member's
|
|
wrapped blob is simply not written during rotation, so they hold a blob that
|
|
decrypts to a stale key — they cannot read items encrypted under the new
|
|
key.
|
|
|
|
### Audit action vocabulary
|
|
|
|
The `relicario org audit` command attributes actions to their verified
|
|
signer (not to the commit author or trailer value). Each event records two
|
|
actors: the **verified** actor resolved from the signing key (authoritative)
|
|
and the actor **claimed** by the `Relicario-Actor` trailer (advisory). When the
|
|
claimed actor disagrees with the verified signer, the event is flagged
|
|
`TAMPERED`. Trailers are advisory metadata; the trustworthy actor is always
|
|
the cryptographically verified signer.
|
|
|
|
Actions live in two groups:
|
|
|
|
- **Membership / collections / lifecycle:** `member-add`, `member-remove`,
|
|
`member-role-change`, `collection-create`, `collection-grant`,
|
|
`collection-revoke`, `key-rotate`, `org-init`, `ownership-transfer`,
|
|
`org-delete`.
|
|
- **Item CRUD:** `item-create`, `item-update`, `item-delete` (soft-delete /
|
|
trash), `item-restore`, `item-purge` — emitted by the `org add` / `edit` /
|
|
`rm` / `restore` / `purge` commands.
|
|
|
|
### Honest limitations
|
|
|
|
The following are deliberate design boundaries, not oversights:
|
|
|
|
- **Shared org master key — reads are not cryptographically scoped per
|
|
collection.** The pre-receive hook scopes *writes* by collection path
|
|
and the CLI filters the manifest to each member's grants, but a single
|
|
org key opens all collection blobs. A member with any grant can, outside
|
|
the CLI, decrypt items from collections they are not granted. For true
|
|
cryptographic separation, use a separate org vault per access boundary.
|
|
Per-collection subkeys are a phase-2 non-goal.
|
|
|
|
- **No read audit.** Git records writes only. A member who reads blobs
|
|
directly leaves no server-visible trace.
|
|
|
|
- **No "hide value."** There is no mechanism to show a member that an item
|
|
exists without revealing its field values on decrypt.
|
|
|
|
- **`delete-org` is a local tombstone in phase 1.** The schema-monotonicity
|
|
check causes the hook to reject protected-file deletion, so an
|
|
`org-delete` action cannot be pushed to a hook-protected remote. The
|
|
deletion is recorded locally only until a future phase addresses it.
|
|
|
|
## Configuration env vars
|
|
|
|
Relicario reads the following environment variables. Each is a trust
|
|
boundary: an attacker who can set them in the user's environment can
|
|
influence Relicario's behavior. They are listed here for security
|
|
reviewers to audit the surface in one place.
|
|
|
|
### User-facing (active in all builds)
|
|
|
|
| Variable | Purpose | Trust |
|
|
|---|---|---|
|
|
| `RELICARIO_IMAGE` | Override the reference-image JPEG path used during vault unlock. | Trusted: filesystem path under the user's control. Read-only; its bytes feed `imgsecret::extract_secret`. |
|
|
| `RELICARIO_GITEA_URL` | Gitea API base URL for `relicario device add`. Equivalent to `--gitea-url`. | Trusted: HTTPS URL. Used only in the device-add code path. |
|
|
| `RELICARIO_GITEA_TOKEN` | Gitea personal-access token. Equivalent to `--gitea-token`. | **Secret**: anyone who can read this env var can manage the user's deploy keys via the Gitea API. The CLI never logs it. |
|
|
| `RELICARIO_GITEA_OWNER` | Gitea repository owner (e.g. `alee`). Equivalent to `--owner`. | Trusted: opaque string. |
|
|
| `RELICARIO_GITEA_REPO` | Gitea repository name (e.g. `vault`). Equivalent to `--repo`. | Trusted: opaque string. |
|
|
|
|
### Debug-only (compiled out of `cargo build --release`)
|
|
|
|
The following variables are gated behind `cfg(debug_assertions)` and
|
|
are **no-ops** in release builds. The env-var lookup is removed by the
|
|
optimiser from any binary built without debug assertions (i.e. the
|
|
standard `--release` profile).
|
|
|
|
| Variable | Purpose |
|
|
|---|---|
|
|
| `RELICARIO_NO_GROUPS_CACHE` | Suppress the plaintext `groups.cache` write. Developer debugging tool for the cache logic. |
|
|
| `RELICARIO_TEST_PASSPHRASE` | Bypass the `rpassword` prompt during integration tests. |
|
|
| `RELICARIO_TEST_ITEM_SECRET` | Bypass the `rpassword` prompt for item-secret fields during integration tests. |
|
|
| `RELICARIO_TEST_BACKUP_PASSPHRASE` | Bypass the `rpassword` prompt for backup export/restore passphrases during integration tests. |
|
|
|
|
---
|
|
|
|
**Next:** [../crates/relicario-core/ARCHITECTURE.md](../crates/relicario-core/ARCHITECTURE.md) — implementation, starting with the platform-agnostic core.
|