a73x

specs/001-gpg-event-signing/plan.md

Ref:   Size: 5.7 KiB

# Implementation Plan: Ed25519 Signing for Event Commits

**Branch**: `001-gpg-event-signing` | **Date**: 2026-03-21 | **Spec**: `specs/001-gpg-event-signing/spec.md`
**Input**: Feature specification from `/specs/001-gpg-event-signing/spec.md`

## Summary

Add Ed25519 signing and verification to all event commits in git-collab. Every event (issues, comments, patches, reviews, etc.) will embed a cryptographic signature and public key in `event.json`. Sync verification rejects unsigned or invalidly-signed events. Uses pure-Rust `ed25519-dalek` crate — no external GPG/SSH binaries. Keypair provisioned via explicit `collab init-key` command.

## Technical Context

**Language/Version**: Rust 2021 edition
**Primary Dependencies**: git2 0.19, clap 4, serde/serde_json 1, chrono 0.4, thiserror 2. New: `ed25519-dalek`, `rand`, `base64`
**Storage**: Git-native (event DAG stored as commits with `event.json` blobs under `refs/collab/`)
**Testing**: `cargo test` with `tempfile` for integration tests
**Target Platform**: Linux (CLI tool)
**Project Type**: CLI tool / library
**Performance Goals**: Signature verification < 1 second per 100 commits (SC-004)
**Constraints**: Pure-Rust crypto, no external binary dependencies, in-process signing
**Scale/Scope**: Single-user local operation with multi-user sync via git remotes

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

Constitution is unconfigured (template only). No gates to enforce. Proceeding.

## Architecture

### Signing Flow

```
User creates event (e.g., issue open)
  → Event struct built (without signature fields)
  → Serialize to canonical JSON (deterministic key ordering)
  → Load Ed25519 private key from ~/.config/git-collab/signing-key
  → Sign canonical JSON bytes
  → Add "signature" (base64) and "pubkey" (base64) fields to Event
  → Serialize final event.json with signature
  → Create git commit as before
```

### Verification Flow (Sync)

```
Fetch remote refs
  → For each incoming commit, read event.json
  → Extract and remove "signature" and "pubkey" fields
  → Reserialize remaining fields to canonical JSON
  → Verify signature against pubkey and canonical bytes
  → If valid: accept commit for reconciliation
  → If invalid/missing: reject entire ref update, report details
```

### Key Management

```
collab init-key
  → Generate Ed25519 keypair using ed25519-dalek + rand
  → Store private key at ~/.config/git-collab/signing-key (base64)
  → Store public key at ~/.config/git-collab/signing-key.pub (base64)
  → Print public key for sharing with collaborators
```

## Project Structure

### Documentation (this feature)

```text
specs/001-gpg-event-signing/
├── plan.md              # This file
├── spec.md              # Feature specification
├── research.md          # Phase 0 output
├── data-model.md        # Phase 1 output
├── quickstart.md        # Phase 1 output
├── contracts/           # Phase 1 output
└── tasks.md             # Phase 2 output (speckit.tasks)
```

### Source Code (repository root)

```text
src/
├── main.rs              # CLI entry point
├── lib.rs               # Library entry, run() dispatcher
├── cli.rs               # Clap command definitions (add init-key)
├── event.rs             # Event/Action structs (add signature, pubkey fields)
├── dag.rs               # DAG operations (integrate signing into commit creation)
├── signing.rs           # NEW: Ed25519 key management, sign, verify functions
├── identity.rs          # Author/signature (unchanged)
├── state.rs             # State materialization (unchanged)
├── issue.rs             # Issue operations (unchanged — signing injected at dag layer)
├── patch.rs             # Patch operations (unchanged — signing injected at dag layer)
├── sync.rs              # Sync logic (add verification before reconciliation)
├── tui.rs               # TUI dashboard (unchanged)
└── error.rs             # Error types (add signing/verification errors)

tests/
├── common/mod.rs        # Test helpers (add key generation helpers)
├── collab_test.rs       # Existing tests (update to use signing)
├── sync_test.rs         # Sync tests (add verification tests)
└── cli_test.rs          # CLI tests (add init-key tests)
```

**Structure Decision**: Single-crate CLI project. New `signing.rs` module contains all Ed25519 logic. Signing is injected at the DAG layer (`dag.rs`) so all event creation paths automatically sign. Verification is added in `sync.rs` before reconciliation.

## Key Design Decisions

1. **Signing at DAG layer, not domain layer**: `dag::create_root_event()` and `dag::append_event()` handle signing so that issue.rs, patch.rs, etc. don't need modification. The signing key is passed as a parameter.

2. **Canonical serialization**: Use `serde_json::to_string()` (not pretty-printed) with sorted keys for the signable payload. The `signature` and `pubkey` fields are added *after* signing, so they're naturally excluded from the signed content.

3. **Two-step serialization**: First serialize Event without signature fields → sign → then serialize SignedEvent (Event + signature + pubkey) as pretty-printed JSON for the blob. This means Event struct stays clean; a wrapper handles the signature fields.

4. **Key storage path**: `~/.config/git-collab/` following XDG conventions. Private key file permissions set to 0o600.

5. **Verification is all-or-nothing per ref**: If any commit in a ref's history fails verification, the entire ref is rejected (FR-008). This is checked by walking the DAG during sync.

## Complexity Tracking

No constitution violations to justify.