specs/003-key-trust-allowlist/tasks.md
Ref: Size: 10.1 KiB
# Tasks: Key Trust Allowlist
**Input**: Design documents from `/specs/003-key-trust-allowlist/`
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
**Tests**: TDD approach -- write tests first, verify they fail, then implement.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
---
## Phase 1: Foundational (Blocking Prerequisites)
**Purpose**: Shared types, error variants, and the trust module skeleton that all user stories depend on.
<!-- sequential -->
- [x] T001 [US1] Add `Untrusted` variant to `VerifyStatus` enum in `src/signing.rs` -- add `Untrusted` to the enum and update any existing match arms that need a wildcard or explicit handling (e.g., Display, PartialEq derive is automatic). No behavioral change yet.
<!-- sequential -->
- [x] T002 [US1] Add `UntrustedKey(String)` variant to `Error` enum in `src/error.rs`.
<!-- sequential -->
- [x] T003 [US1] Create `src/trust.rs` with `TrustedKey` and `TrustPolicy` structs, `trusted_keys_path()`, `load_trust_policy()`, `is_key_trusted()`, and `validate_pubkey()` function stubs (return `todo!()`). Register `pub mod trust;` in `src/lib.rs`.
**Checkpoint**: Foundation types compiled. All user stories can now proceed.
---
## Phase 2: User Story 1 -- Add Trusted Keys (Priority: P1)
**Goal**: Users can add a teammate's public key to a trusted keys list via `collab key add`.
**Independent Test**: Run `collab key add <pubkey>`, then verify the key appears via `collab key list`.
### Tests for User Story 1
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
<!-- parallel-group: 1 -->
- [x] T004 [P] [US1] Unit tests for `validate_pubkey()` in `src/trust.rs` (`#[cfg(test)]` module) -- test valid base64 32-byte Ed25519 key accepted, invalid base64 rejected, wrong byte length rejected, invalid Ed25519 point rejected.
- [x] T005 [P] [US1] Unit tests for `load_trust_policy()` and `save_trusted_key()` in `src/trust.rs` (`#[cfg(test)]` module) -- test: file not found returns `TrustPolicy::Unconfigured`, empty file returns `Configured(empty)`, file with keys+comments+blanks parses correctly, malformed lines skipped with warning, duplicates deduplicated.
- [x] T006 [P] [US1] Integration test for `collab key add` in `tests/trust_test.rs` -- test: add valid key writes to `.git/collab/trusted-keys`, add duplicate key prints already-trusted message, add invalid key returns error, `--self` reads from signing-key.pub, `--self` with PUBKEY arg errors, add with `--label` stores label.
### Implementation for User Story 1
<!-- sequential -->
- [x] T007 [US1] Implement `validate_pubkey()` in `src/trust.rs` -- base64 decode, check 32 bytes, `VerifyingKey::from_bytes()`. Return `Result<(), Error>` with descriptive error messages.
<!-- parallel-group: 2 -->
- [x] T008 [P] [US1] Implement `trusted_keys_path()`, `load_trust_policy()`, and file parsing in `src/trust.rs` -- parse `<base64-key> <optional label>` format, skip `#` comments and blank lines, warn on malformed lines, deduplicate on load. Return `TrustPolicy::Unconfigured` when file missing, `TrustPolicy::Configured(Vec<TrustedKey>)` when present.
- [x] T009 [P] [US1] Implement `save_trusted_key()` and `remove_trusted_key()` in `src/trust.rs` -- append key+label to file (create `.git/collab/` dir if needed), check for duplicates before adding. `remove_trusted_key()` rewrites file without the matching entry.
<!-- sequential -->
- [x] T010 [US1] Add `KeyCmd` subcommand enum to `src/cli.rs` -- add `Key(KeyCmd)` variant to `Commands` enum. `KeyCmd` has `Add { pubkey: Option<String>, self_key: bool, label: Option<String> }`, `List`, and `Remove { pubkey: String }` following the existing `IssueCmd`/`PatchCmd` pattern.
<!-- sequential -->
- [x] T011 [US1] Wire `Commands::Key(KeyCmd::Add { .. })` handler in `src/lib.rs` -- implement the `key add` logic: handle `--self` (load from `signing_key_dir()/signing-key.pub`), validate key, check duplicate, save, print confirmation. Wire only `Add` for now; `List` and `Remove` can return `todo!()`.
**Checkpoint**: `collab key add` works end-to-end. Tests from T004-T006 pass.
---
## Phase 3: User Story 2 -- Reject Untrusted Keys During Sync (Priority: P2)
**Goal**: Events signed by keys not in the trusted keys list are rejected during sync.
**Independent Test**: Sync from a remote with events signed by an untrusted key and verify rejection.
### Tests for User Story 2
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
<!-- parallel-group: 3 -->
- [x] T012 [P] [US2] Unit tests for `check_trust()` in `src/trust.rs` (`#[cfg(test)]` module) -- test: `TrustPolicy::Unconfigured` returns all results unchanged, `Configured` with key in set returns `Valid`, `Configured` with key not in set returns `Untrusted`, `Missing`/`Invalid` statuses pass through unchanged, empty configured set rejects all valid signatures.
- [x] T013 [P] [US2] Integration test for sync trust rejection in `tests/trust_test.rs` -- test: sync accepts events from trusted key, sync rejects ref when any commit has untrusted key (whole-ref rejection), sync with no trusted keys file falls back to accept-all with warning, rejection message includes the untrusted key's base64 value.
### Implementation for User Story 2
<!-- sequential -->
- [x] T014 [US2] Implement `check_trust()` in `src/trust.rs` -- takes `&[SignatureVerificationResult]` and `&TrustPolicy`, returns new `Vec<SignatureVerificationResult>` with `Valid` statuses changed to `Untrusted` for keys not in the trusted set. `Unconfigured` passes through unchanged.
<!-- sequential -->
- [x] T015 [US2] Integrate trust checking into `reconcile_refs()` in `src/sync.rs` -- after `verify_ref()` succeeds, load `TrustPolicy` via `trust::load_trust_policy()`, call `trust::check_trust()` on results, reject ref if any result is `Untrusted` (print untrusted key value). When `Unconfigured`, print warning once per sync. Add `use crate::trust;` import.
**Checkpoint**: Sync enforces key trust. Events from untrusted keys are rejected. Tests from T012-T013 pass.
---
## Phase 4: User Story 3 -- Manage Trusted Keys (Priority: P3)
**Goal**: Users can list and remove trusted keys to maintain the allowlist.
**Independent Test**: Add keys, list them, remove one, verify the list reflects the removal.
### Tests for User Story 3
> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
<!-- parallel-group: 4 -->
- [x] T016 [P] [US3] Integration tests for `collab key list` in `tests/trust_test.rs` -- test: list with no file prints "No trusted keys configured.", list with keys shows each key and label, list with key without label shows key only.
- [x] T017 [P] [US3] Integration tests for `collab key remove` in `tests/trust_test.rs` -- test: remove existing key prints confirmation with label, remove non-existent key returns error, remove last key leaves empty file (still `Configured`).
### Implementation for User Story 3
<!-- sequential -->
- [x] T018 [US3] Wire `Commands::Key(KeyCmd::List)` handler in `src/lib.rs` -- load trust policy, print each key and label (or "No trusted keys configured." if unconfigured/empty).
<!-- sequential -->
- [x] T019 [US3] Wire `Commands::Key(KeyCmd::Remove { .. })` handler in `src/lib.rs` -- call `trust::remove_trusted_key()`, print removed key and label, or error if not found.
**Checkpoint**: Full key lifecycle (add/list/remove) works. All tests pass.
---
## Phase 5: Polish & Cross-Cutting Concerns
**Purpose**: Edge cases, backward compatibility, and final validation.
<!-- parallel-group: 5 -->
- [x] T020 [P] Verify existing tests still pass (`tests/collab_test.rs`, `tests/sync_test.rs`) -- no trusted keys file means fallback behavior, no regressions.
- [x] T021 [P] Add edge case tests in `tests/trust_test.rs` -- corrupted trusted keys file (some valid, some invalid lines), key rotation scenario (old key removed, new key added, old events still valid if old key re-trusted).
<!-- sequential -->
- [x] T022 Run full test suite (`cargo test`) and fix any compilation or test failures.
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Foundational)**: No dependencies -- start immediately. T001 -> T002 -> T003 strictly sequential (each depends on prior compilation).
- **Phase 2 (US1 Add Keys)**: Depends on Phase 1 completion.
- **Phase 3 (US2 Sync Trust)**: Depends on Phase 2 (needs `trust.rs` file I/O and `VerifyStatus::Untrusted`).
- **Phase 4 (US3 List/Remove)**: Depends on Phase 2 (needs `trust.rs` CRUD functions). Can run in parallel with Phase 3.
- **Phase 5 (Polish)**: Depends on Phases 3 and 4.
### Within Each Phase
- Tests MUST be written and FAIL before implementation begins.
- [P] tasks within the same parallel group can run concurrently.
- Sequential tasks must complete in listed order.
### Parallel Opportunities
- **Phase 2**: T004, T005, T006 (tests, different files/modules). T008, T009 (different functions, no overlap).
- **Phase 3**: T012, T013 (tests, different files). T014 then T015 sequential (T015 depends on T014).
- **Phase 4**: T016, T017 (tests, same file but independent test functions). T018 then T019 sequential.
- **Phase 3 and Phase 4** can run in parallel if staffed (different files: `src/sync.rs` vs `src/lib.rs`).
- **Phase 5**: T020, T021 parallel (different test files).
### File Ownership by Task
| File | Tasks |
|------|-------|
| `src/signing.rs` | T001 |
| `src/error.rs` | T002 |
| `src/trust.rs` | T003, T004, T005, T007, T008, T009, T012, T014 |
| `src/cli.rs` | T010 |
| `src/lib.rs` | T003 (mod declaration), T011, T018, T019 |
| `src/sync.rs` | T015 |
| `tests/trust_test.rs` | T006, T013, T016, T017, T021 |
---
## Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- Each user story should be independently completable and testable
- Verify tests fail before implementing
- Commit after each task or logical group
- Stop at any checkpoint to validate story independently