a73x

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).