specs/003-key-trust-allowlist/research.md
Ref: Size: 3.9 KiB
# Research: Key Trust Allowlist
**Date**: 2026-03-21 | **Feature**: 003-key-trust-allowlist
## Codebase Analysis
### Current Signing Architecture
The signing system lives in `src/signing.rs` and provides:
- **Key management**: `generate_keypair()`, `load_signing_key()`, `load_verifying_key()` — keys stored at `~/.config/git-collab/signing-key{,.pub}` as base64-encoded Ed25519 bytes.
- **Event signing**: `sign_event()` produces a `SignedEvent` (event + base64 signature + base64 pubkey). The pubkey is embedded per-event, not referenced from a keyring.
- **Verification**: `verify_signed_event()` checks the signature against the embedded pubkey. `verify_ref()` walks the DAG for a ref and returns `Vec<SignatureVerificationResult>` with `status: VerifyStatus` and optional `pubkey: Option<String>`.
- **VerifyStatus**: `Valid`, `Invalid`, `Missing` — no trust/authorization concept.
### Current Sync Flow
`src/sync.rs::sync()`:
1. `git fetch` collab refs into `refs/collab/sync/{issues,patches}/*`
2. `reconcile_refs()` for issues and patches
3. `git push` local collab refs
4. Clean up sync refs
`reconcile_refs()`:
1. Iterates sync refs
2. Calls `signing::verify_ref(repo, remote_ref)` for each
3. Rejects the entire ref if **any** commit has `status != Valid`
4. If all valid: reconcile (merge DAGs) or adopt new ref
Key observation: the rejection logic at step 3 is where trust checking naturally fits. The `results` vector already contains `pubkey: Option<String>` for each verified commit.
### CLI Structure
`src/cli.rs` uses clap derive with a top-level `Commands` enum. Subcommand groups use nested enums (`IssueCmd`, `PatchCmd`). Adding `Key(KeyCmd)` follows the established pattern.
`src/lib.rs::run()` dispatches commands via pattern matching. Adding a `Commands::Key(cmd)` arm is straightforward.
### Error Handling
`src/error.rs` uses `thiserror` with variants for Git, JSON, IO, Signing, Verification, Cmd, and KeyNotFound. New trust-related errors fit naturally as additional variants.
### Storage Patterns
The project uses `.git/collab/` for local state (not git objects). The trusted keys file at `.git/collab/trusted-keys` follows this convention. The directory may not exist yet for a given repo; creation logic should mirror any existing patterns.
### Dependencies Already Available
- `base64` 0.22 — for key encoding/decoding
- `ed25519-dalek` 2 — for `VerifyingKey::from_bytes()` validation
- `dirs` 5 — for locating `~/.config/git-collab/signing-key.pub` (used by `--self`)
- `std::fs` — for file I/O (no serde needed; plain text format)
No new crate dependencies are required.
## Design Considerations
### Where to Load Trusted Keys
The trusted keys file path depends on `repo.path()` (the `.git/` directory). In `reconcile_refs()`, the repo is already available. The trust module should expose:
```rust
pub fn trusted_keys_path(repo: &Repository) -> PathBuf
pub fn load_trusted_keys(repo: &Repository) -> Result<Option<HashSet<String>>, Error>
```
Returning `Option<HashSet>` — `None` means no file exists (fallback mode), `Some(set)` means enforce trust.
### Backward Compatibility
- Unsigned events (`VerifyStatus::Missing`) are already rejected by `reconcile_refs()`. The spec says trust policy only applies to signed commits (FR-002b), but the current code already rejects Missing. No change needed — unsigned commits are rejected regardless.
- When no trusted keys file exists, behavior is identical to current (all valid sigs accepted). A warning is printed once per sync.
### Test Strategy
- **Unit tests** in `src/trust.rs`: parse trusted keys file, validate keys, check membership, handle edge cases (comments, blank lines, duplicates, malformed).
- **Integration tests** in `tests/trust_test.rs`: end-to-end flow using temp repos — add key, sync with trusted/untrusted events, verify acceptance/rejection.
- Existing tests in `tests/collab_test.rs` and `tests/sync_test.rs` should continue to pass (no trusted keys file = fallback mode).