v0.5.0 Plan A — Security Fixes + Repo Cleanup. 7 commits, ~800 net
insertions across the Rust workspace. Four items delivered:
- S1 (HIGH-severity authentication bypass fix): rewrite verify_commit
in relicario-server. The previous implementation accepted any
GOODSIG/Good signature line on stderr, ignoring whether the signing
key was registered or revoked. The new implementation:
* builds a temp gpg.ssh.allowedSignersFile from devices.json at the
commit (no global git-config mutation)
* parses the SHA-256 fingerprint from `git verify-commit --raw`
stderr via regex
* checks revocation FIRST (revoked entries may have been removed
from devices.json), with the historical-commit case
(committer_ts < revoked_at) explicitly allowed
* uses committer date (GIT_COMMITTER_DATE / `git show -s
--format=%ct`), not author date or wall clock
* tightened the bootstrap guard to require BOTH devices and revoked
to be empty (closes an empty-devices.json privilege-escalation
route present in the original code)
* 4 acceptance integration tests build real on-disk repos with
SSH-signed commits and verify each scenario
- S2 (tar archive path-traversal hardening): replace
tar::Archive::unpack with safe_unpack_git_archive. Located in
relicario-core (per-spec, so integration tests can reach it without
the bytes-in/bytes-out invariant breaking). Validates each entry's
type (rejects symlinks/hardlinks), path components (rejects '..',
RootDir, Windows drive Prefix), and declared size (rejects
individual or cumulative > 100×compressed-or-1-GiB whichever is
lower). The CLI's restore path adds a paranoid OS-level
starts_with(.git/) check on the joined destination as
defense-in-depth even after textual validation. 5 acceptance tests
cover path traversal, symlinks, oversized headers (header claim of
2 GiB tested without allocating disk).
- S3 (RELICARIO_* env-var audit): docs/SECURITY.md gains a
"Configuration env vars" section enumerating each variable, its
purpose, and trust assumption. Active-in-all-builds variables
(RELICARIO_IMAGE, RELICARIO_GITEA_*) are documented; debug-only
variables (RELICARIO_NO_GROUPS_CACHE, RELICARIO_TEST_*) are gated
behind cfg(debug_assertions) so the env-var lookup is removed from
--release binaries.
- C1 (stale feature branch prune): 5 merged feature branches and
3 worktrees pruned interactively per dev report.
- Bonus: 4d02a50 fixes pre-existing clippy warnings across
crates/relicario-{core,cli} (deref operators, Option::is_none_or
vs map_or(true, ...), iter_mut().enumerate() patterns,
div_ceil()) so the workspace builds clean under `-D warnings`.
Merge resolution: docs/SECURITY.md had a conflict where main's F11/F12
(Device Authentication paragraph naming relicario-server + simplified
"Device registration is optional" line) collided with Plan A's S3
section. Resolved by keeping both — F11/F12's wording for the
Device Authentication section, then Plan A's "Configuration env vars"
section appended below.
Cargo.lock regenerated. The previous committed lock was stale since
commit 8855078 (--totp-qr); cargo test on both devs' worktrees
produced identical regenerated locks. Plan A genuinely added regex +
tempfile to relicario-server (both already transitively present from
relicario-cli), so no new top-level deps; the Cargo.lock churn is
catch-up of crate-version bumps that have happened since the last
commit-of-record.
Tests: 248 cargo tests pass; extension tests unchanged (336/8 with 8
pre-existing device-auth scaffolding failures).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
105 lines
4.5 KiB
Markdown
105 lines
4.5 KiB
Markdown
# Relicario Security Model
|
|
|
|
## 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.
|
|
|
|
## 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. |
|