a73x

specs/003-key-trust-allowlist/spec.md

Ref:   Size: 9.0 KiB

# Feature Specification: Key Trust Allowlist

**Feature Branch**: `003-key-trust-allowlist`
**Created**: 2026-03-21
**Status**: Draft
**Input**: User description: "Add a key trust/allowlist mechanism for sync verification. Currently any valid Ed25519 key is accepted during sync — an attacker with remote write access can forge events with their own key. Add a known-keys file that lists trusted public keys, and reject events signed by unknown keys during sync. Users should be able to add/remove trusted keys via CLI commands."

## User Scenarios & Testing

### User Story 1 - Add Trusted Keys (Priority: P1)

As a collaborator, I can add a teammate's public key to a trusted keys list so that events signed by their key are accepted during sync.

**Why this priority**: Without the ability to register trusted keys, there is no allowlist to verify against. This is the foundational action that enables the entire trust model.

**Independent Test**: Can be fully tested by running `collab key add <pubkey>`, then verifying the key appears in the trusted keys list via `collab key list`.

**Acceptance Scenarios**:

1. **Given** a collaborator has another user's base64-encoded public key, **When** they run `collab key add <pubkey>`, **Then** the key is stored in the project's trusted keys file and a confirmation message is shown.
2. **Given** a collaborator runs `collab key add --self`, **Then** the user's own public key is read from the local signing key and added to the trusted list.
3. **Given** a collaborator runs `collab key add` with an invalid or malformed key, **Then** the command fails with a clear error message.
4. **Given** a collaborator runs `collab key add` with a key that is already trusted, **Then** the command reports that the key is already in the list (no duplicate).

---

### User Story 2 - Reject Untrusted Keys During Sync (Priority: P2)

As a collaborator, when I sync from a remote, events signed by keys not in my trusted keys list are rejected, preventing impersonation by attackers with remote write access.

**Why this priority**: This is the core security gate. Without rejection of untrusted keys, the allowlist has no enforcement and the trust model is incomplete.

**Independent Test**: Can be tested by syncing from a remote that contains events signed by an untrusted key and verifying they are rejected with a clear message identifying the unknown key.

**Acceptance Scenarios**:

1. **Given** a remote contains events signed by a trusted key, **When** I sync, **Then** the events are accepted normally.
2. **Given** a remote contains events signed by a key not in the trusted keys list, **When** I sync, **Then** the ref is rejected and the user is warned with the unknown public key value so they can decide whether to trust it.
3. **Given** a remote contains events from multiple authors (some trusted, some not), **When** I sync, **Then** only refs containing untrusted signatures are rejected; refs with all-trusted signatures sync normally.
4. **Given** the trusted keys file does not exist (no keys added yet), **When** I sync, **Then** the system falls back to the current behavior (accept any valid signature) and warns the user that no trusted keys are configured.

---

### User Story 3 - Manage Trusted Keys (Priority: P3)

As a collaborator, I can list and remove trusted keys to maintain the allowlist over time — for example, removing a key when a teammate leaves the project.

**Why this priority**: Key lifecycle management is important for long-term security but is not required for the initial trust model to function.

**Independent Test**: Can be tested by adding keys, listing them, removing one, and verifying the list reflects the removal.

**Acceptance Scenarios**:

1. **Given** trusted keys have been added, **When** I run `collab key list`, **Then** all trusted public keys are listed with any associated labels.
2. **Given** a trusted key exists, **When** I run `collab key remove <pubkey>`, **Then** the key is removed from the trusted list and a confirmation is shown.
3. **Given** I run `collab key remove` with a key not in the list, **Then** the command fails with a clear error message.

---

### Edge Cases

- What happens if the trusted keys file is corrupted or has invalid entries? The system skips invalid lines with a warning and processes valid entries.
- What happens if a user's key is rotated (old key replaced with new)? The old key must be explicitly removed and the new key added. Events signed with the old key remain valid if the old key is still trusted.
- What happens during the first sync on a fresh clone with no trusted keys file? The system falls back to accepting any valid signature and warns that no trust policy is configured.
- Can trusted keys be shared across collaborators? The trusted keys file is stored locally per-repository, not synced. Each collaborator maintains their own list.

## Clarifications

### Session 2026-03-21

- Q: Should the entire ref be rejected if any commit is from an untrusted key? → A: Yes, reject the entire ref (consistent with current verify_ref behavior and DAG integrity).
- Q: Should `collab key add --self` auto-add the user's own pubkey? → A: Yes, add a `--self` flag that reads from `~/.config/git-collab/signing-key.pub`.
- Q: What is the trusted keys file format? → A: SSH authorized_keys style — `<base64-key> <label>`, first space delimits. Lines starting with `#` are comments, empty lines skipped.
- Q: How to handle unsigned legacy commits when trust file exists? → A: Only enforce trust on signed commits (those with a `pubkey` field). Unsigned commits are outside the trust policy scope.
- Q: Should key removal support labels or require confirmation? → A: Remove by pubkey only, no confirmation. Print the removed key and label so user can verify.

## Requirements

### Functional Requirements

- **FR-001**: System MUST store trusted public keys in a project-local file (not user-global), so different projects can have different trust policies.
- **FR-002**: System MUST provide a `collab key add <pubkey>` command that appends a base64-encoded Ed25519 public key to the trusted keys file. A `--self` flag MUST read the user's own public key from the local signing key file and add it.
- **FR-002a**: System MUST reject the entire ref during sync if any commit on it is signed by an untrusted key (whole-ref rejection for DAG integrity).
- **FR-002b**: System MUST only enforce trust policy on signed commits (those with a `pubkey` field). Unsigned legacy commits are outside the trust policy scope and are not subject to key trust checks.
- **FR-003**: System MUST provide a `collab key list` command that displays all currently trusted public keys.
- **FR-004**: System MUST provide a `collab key remove <pubkey>` command that removes a public key from the trusted keys file.
- **FR-005**: System MUST reject event commits during sync whose signature was made by a key not present in the trusted keys file, reporting the unknown key's base64 value.
- **FR-006**: System MUST accept event commits during sync whose signature was made by a key present in the trusted keys file, provided the signature is cryptographically valid.
- **FR-007**: System MUST fall back to current behavior (accept any valid signature) when no trusted keys file exists, and print a warning advising the user to configure trusted keys.
- **FR-008**: System MUST validate that a key being added is a syntactically valid base64-encoded 32-byte Ed25519 public key, rejecting malformed input.
- **FR-009**: System MUST prevent duplicate keys in the trusted keys file.
- **FR-010**: System MUST allow the `collab key add` command to accept an optional label for the key (e.g., `collab key add <pubkey> --label "Alice"`), displayed in `collab key list` output.

### Key Entities

- **Trusted Keys File**: A project-local file listing base64-encoded Ed25519 public keys that are authorized to sign events. SSH authorized_keys style format: one key per line, first space delimits key from label. Lines starting with `#` are comments, empty lines are skipped.
- **Trusted Key Entry**: A single line in the trusted keys file consisting of a base64-encoded public key, optionally followed by a space and a human-readable label (label may contain spaces).

## Success Criteria

### Measurable Outcomes

- **SC-001**: 100% of events signed by untrusted keys are rejected during sync when a trusted keys file is configured.
- **SC-002**: 0% of events signed by trusted keys are incorrectly rejected.
- **SC-003**: Users receive the unknown key's public value in rejection messages, enabling a single copy-paste to add trust if desired.
- **SC-004**: Key management operations (add, list, remove) complete in under 1 second.

## Assumptions

- The trusted keys file is stored locally in the repository's git directory (e.g., under `.git/collab/trusted-keys`), not committed to the main branch.
- Each collaborator maintains their own trusted keys list independently.
- Public keys are exchanged out-of-band (same assumption as the signing feature).
- The user's own public key is not automatically added to the trusted list — it must be explicitly added like any other key.